[Gekidou] Add Latex support (#6195)

* Add Latex support

* Markdown memoization

* feedback review

* feedback review 2
This commit is contained in:
Elias Nahum
2022-04-28 12:27:10 -04:00
committed by GitHub
parent e047106bac
commit 22a173ec97
24 changed files with 1414 additions and 546 deletions

View File

@@ -89,7 +89,7 @@ const AtMention = ({
return user.mentionKeys;
}, [currentUserId, mentionKeys, user]);
const goToUserProfile = useCallback(() => {
const goToUserProfile = () => {
const screen = 'UserProfile';
const title = intl.formatMessage({id: 'mobile.routes.user_profile', defaultMessage: 'Profile'});
const passProps = {
@@ -109,7 +109,7 @@ const AtMention = ({
};
showModal(screen, title, passProps, options);
}, [user]);
};
const handleLongPress = useCallback(() => {
if (managedConfig?.copyAndPasteProtection !== 'true') {
@@ -230,4 +230,4 @@ const AtMention = ({
);
};
export default React.memo(AtMention);
export default AtMention;

View File

@@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useCallback} from 'react';
import React from 'react';
import {useIntl} from 'react-intl';
import {StyleProp, Text, TextStyle} from 'react-native';
@@ -65,7 +65,7 @@ const ChannelMention = ({
const serverUrl = useServerUrl();
const channel = getChannelFromChannelName(channelName, channels, channelMentions, team.name);
const handlePress = useCallback(preventDoubleTap(async () => {
const handlePress = preventDoubleTap(async () => {
let c = channel;
if (!c?.id && c?.display_name) {
@@ -90,7 +90,7 @@ const ChannelMention = ({
await dismissAllModals();
await popToRoot();
}
}), [channel?.display_name, channel?.id]);
});
if (!channel) {
return <Text style={textStyle}>{`~${channelName}`}</Text>;

View File

@@ -0,0 +1,27 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
import withObservables from '@nozbe/with-observables';
import React from 'react';
import {of as of$} from 'rxjs';
import {switchMap} from 'rxjs/operators';
import {observeConfig} from '@queries/servers/system';
import Markdown from './markdown';
import type {WithDatabaseArgs} from '@typings/database/database';
const enhanced = withObservables([], ({database}: WithDatabaseArgs) => {
const config = observeConfig(database);
const enableLatex = config.pipe(switchMap((c) => of$(c?.EnableLatex === 'true')));
const enableInlineLatex = config.pipe(switchMap((c) => of$(c?.EnableInlineLatex === 'true')));
return {
enableLatex,
enableInlineLatex,
};
});
export default React.memo(withDatabase(enhanced(Markdown)));

View File

@@ -1,510 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Parser, Node} from 'commonmark';
import Renderer from 'commonmark-react-renderer';
import React, {PureComponent, ReactElement} from 'react';
import {GestureResponderEvent, Platform, StyleProp, Text, TextStyle, View} from 'react-native';
import Emoji from '@components/emoji';
import FormattedText from '@components/formatted_text';
import Hashtag from '@components/markdown/hashtag';
import {blendColors, concatStyles, makeStyleSheetFromTheme} from '@utils/theme';
import {getScheme} from '@utils/url';
import AtMention from './at_mention';
import ChannelMention, {ChannelMentions} from './channel_mention';
import MarkdownBlockQuote from './markdown_block_quote';
import MarkdownCodeBlock from './markdown_code_block';
import MarkdownImage from './markdown_image';
import MarkdownLink from './markdown_link';
import MarkdownList from './markdown_list';
import MarkdownListItem from './markdown_list_item';
import MarkdownTable from './markdown_table';
import MarkdownTableCell, {MarkdownTableCellProps} from './markdown_table_cell';
import MarkdownTableImage from './markdown_table_image';
import MarkdownTableRow, {MarkdownTableRowProps} from './markdown_table_row';
import {addListItemIndices, combineTextNodes, highlightMentions, pullOutImages} from './transform';
import type {
MarkdownAtMentionRenderer, MarkdownBaseRenderer, MarkdownBlockStyles, MarkdownChannelMentionRenderer,
MarkdownEmojiRenderer, MarkdownImageRenderer, MarkdownTextStyles, UserMentionKey,
} from '@typings/global/markdown';
type MarkdownProps = {
autolinkedUrlSchemes?: string[];
baseTextStyle: StyleProp<TextStyle>;
blockStyles: MarkdownBlockStyles;
channelMentions?: ChannelMentions;
disableAtMentions?: boolean;
disableAtChannelMentionHighlight?: boolean;
disableChannelLink?: boolean;
disableGallery?: boolean;
disableHashtags?: boolean;
imagesMetadata?: Record<string, PostImage>;
isEdited?: boolean;
isReplyPost?: boolean;
isSearchResult?: boolean;
layoutWidth?: number;
location?: string;
mentionKeys?: UserMentionKey[];
minimumHashtagLength?: number;
onPostPress?: (event: GestureResponderEvent) => void;
postId?: string;
textStyles: MarkdownTextStyles;
theme: Theme;
value: string | number;
}
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
// Android has trouble giving text transparency depending on how it's nested,
// so we calculate the resulting colour manually
const editedOpacity = Platform.select({
ios: 0.3,
android: 1.0,
});
const editedColor = Platform.select({
ios: theme.centerChannelColor,
android: blendColors(theme.centerChannelBg, theme.centerChannelColor, 0.3),
});
return {
block: {
alignItems: 'flex-start',
flexDirection: 'row',
flexWrap: 'wrap',
},
editedIndicatorText: {
color: editedColor,
opacity: editedOpacity,
},
atMentionOpacity: {
opacity: 1,
},
};
});
class Markdown extends PureComponent<MarkdownProps> {
static defaultProps = {
textStyles: {},
blockStyles: {},
disableHashtags: false,
disableAtMentions: false,
disableAtChannelMentionHighlight: false,
disableChannelLink: false,
disableGallery: false,
layoutWidth: undefined,
value: '',
minimumHashtagLength: 3,
};
private parser: Parser;
private renderer: Renderer.Renderer;
constructor(props: MarkdownProps) {
super(props);
this.parser = this.createParser();
this.renderer = this.createRenderer();
}
createParser = () => {
return new Parser({
urlFilter: this.urlFilter,
minimumHashtagLength: this.props.minimumHashtagLength,
});
};
urlFilter = (url: string) => {
const scheme = getScheme(url);
return !scheme || this.props.autolinkedUrlSchemes?.indexOf(scheme) !== -1;
};
createRenderer = () => {
const renderers: any = {
text: this.renderText,
emph: Renderer.forwardChildren,
strong: Renderer.forwardChildren,
del: Renderer.forwardChildren,
code: this.renderCodeSpan,
link: this.renderLink,
image: this.renderImage,
atMention: this.renderAtMention,
channelLink: this.renderChannelLink,
emoji: this.renderEmoji,
hashtag: this.renderHashtag,
latexinline: this.renderParagraph,
paragraph: this.renderParagraph,
heading: this.renderHeading,
codeBlock: this.renderCodeBlock,
blockQuote: this.renderBlockQuote,
list: this.renderList,
item: this.renderListItem,
hardBreak: this.renderHardBreak,
thematicBreak: this.renderThematicBreak,
softBreak: this.renderSoftBreak,
htmlBlock: this.renderHtml,
htmlInline: this.renderHtml,
table: this.renderTable,
table_row: this.renderTableRow,
table_cell: this.renderTableCell,
mention_highlight: Renderer.forwardChildren,
editedIndicator: this.renderEditedIndicator,
};
return new Renderer({
renderers,
renderParagraphsInLists: true,
getExtraPropsForNode: this.getExtraPropsForNode,
});
};
getExtraPropsForNode = (node: any) => {
const extraProps: Record<string, any> = {
continue: node.continue,
index: node.index,
};
if (node.type === 'image') {
extraProps.reactChildren = node.react.children;
extraProps.linkDestination = node.linkDestination;
extraProps.size = node.size;
}
return extraProps;
};
computeTextStyle = (baseStyle: StyleProp<TextStyle>, context: any) => {
const {textStyles} = this.props;
type TextType = keyof typeof textStyles;
const contextStyles: TextStyle[] = context.map((type: any) => textStyles[type as TextType]).filter((f: any) => f !== undefined);
return contextStyles.length ? concatStyles(baseStyle, contextStyles) : baseStyle;
};
renderText = ({context, literal}: MarkdownBaseRenderer) => {
if (context.indexOf('image') !== -1) {
// If this text is displayed, it will be styled by the image component
return (
<Text testID='markdown_text'>
{literal}
</Text>
);
}
// Construct the text style based off of the parents of this node since RN's inheritance is limited
const style = this.computeTextStyle(this.props.baseTextStyle, context);
return (
<Text
testID='markdown_text'
style={style}
>
{literal}
</Text>
);
};
renderCodeSpan = ({context, literal}: MarkdownBaseRenderer) => {
const {baseTextStyle, textStyles: {code}} = this.props;
return <Text style={this.computeTextStyle([baseTextStyle, code], context)}>{literal}</Text>;
};
renderImage = ({linkDestination, context, src, size}: MarkdownImageRenderer) => {
if (!this.props.imagesMetadata) {
return null;
}
if (context.indexOf('table') !== -1) {
// We have enough problems rendering images as is, so just render a link inside of a table
return (
<MarkdownTableImage
disabled={this.props.disableGallery ?? Boolean(!this.props.location)}
imagesMetadata={this.props.imagesMetadata}
location={this.props.location}
postId={this.props.postId!}
source={src}
/>
);
}
return (
<MarkdownImage
disabled={this.props.disableGallery ?? Boolean(!this.props.location)}
errorTextStyle={[this.computeTextStyle(this.props.baseTextStyle, context), this.props.textStyles.error]}
layoutWidth={this.props.layoutWidth}
linkDestination={linkDestination}
imagesMetadata={this.props.imagesMetadata}
isReplyPost={this.props.isReplyPost}
location={this.props.location}
postId={this.props.postId!}
source={src}
sourceSize={size}
/>
);
};
renderAtMention = ({context, mentionName}: MarkdownAtMentionRenderer) => {
if (this.props.disableAtMentions) {
return this.renderText({context, literal: `@${mentionName}`});
}
const style = getStyleSheet(this.props.theme);
return (
<AtMention
disableAtChannelMentionHighlight={this.props.disableAtChannelMentionHighlight}
mentionStyle={this.props.textStyles.mention}
textStyle={[this.computeTextStyle(this.props.baseTextStyle, context), style.atMentionOpacity]}
isSearchResult={this.props.isSearchResult}
mentionName={mentionName}
onPostPress={this.props.onPostPress}
mentionKeys={this.props.mentionKeys}
/>
);
};
renderChannelLink = ({context, channelName}: MarkdownChannelMentionRenderer) => {
if (this.props.disableChannelLink) {
return this.renderText({context, literal: `~${channelName}`});
}
return (
<ChannelMention
linkStyle={this.props.textStyles.link}
textStyle={this.computeTextStyle(this.props.baseTextStyle, context)}
channelName={channelName}
channelMentions={this.props.channelMentions}
/>
);
};
renderEmoji = ({context, emojiName, literal}: MarkdownEmojiRenderer) => {
return (
<Emoji
emojiName={emojiName}
literal={literal}
testID='markdown_emoji'
textStyle={this.computeTextStyle(this.props.baseTextStyle, context)}
/>
);
};
renderHashtag = ({context, hashtag}: {context: string[]; hashtag: string}) => {
if (this.props.disableHashtags) {
return this.renderText({context, literal: `#${hashtag}`});
}
return (
<Hashtag
hashtag={hashtag}
linkStyle={this.props.textStyles.link}
/>
);
};
renderParagraph = ({children, first}: {children: ReactElement[]; first: boolean}) => {
if (!children || children.length === 0) {
return null;
}
const style = getStyleSheet(this.props.theme);
const blockStyle = [style.block];
if (!first) {
blockStyle.push(this.props.blockStyles.adjacentParagraph);
}
return (
<View style={blockStyle}>
<Text>
{children}
</Text>
</View>
);
};
renderHeading = ({children, level}: {children: ReactElement; level: string}) => {
const {textStyles} = this.props;
const containerStyle = [
getStyleSheet(this.props.theme).block,
textStyles[`heading${level}`],
];
const textStyle = textStyles[`heading${level}Text`];
return (
<View style={containerStyle}>
<Text style={textStyle}>
{children}
</Text>
</View>
);
};
renderCodeBlock = (props: any) => {
// These sometimes include a trailing newline
const content = props.literal.replace(/\n$/, '');
return (
<MarkdownCodeBlock
content={content}
language={props.language}
textStyle={this.props.textStyles.codeBlock}
/>
);
};
renderBlockQuote = ({children, ...otherProps}: any) => {
return (
<MarkdownBlockQuote
iconStyle={this.props.blockStyles.quoteBlockIcon}
{...otherProps}
>
{children}
</MarkdownBlockQuote>
);
};
renderList = ({children, start, tight, type}: any) => {
return (
<MarkdownList
ordered={type !== 'bullet'}
start={start}
tight={tight}
>
{children}
</MarkdownList>
);
};
renderListItem = ({children, context, ...otherProps}: any) => {
const level = context.filter((type: string) => type === 'list').length;
return (
<MarkdownListItem
bulletStyle={this.props.baseTextStyle}
level={level}
{...otherProps}
>
{children}
</MarkdownListItem>
);
};
renderHardBreak = () => {
return <Text>{'\n'}</Text>;
};
renderThematicBreak = () => {
return (
<View
style={this.props.blockStyles.horizontalRule}
testID='markdown_thematic_break'
/>
);
};
renderSoftBreak = () => {
return <Text>{'\n'}</Text>;
};
renderHtml = (props: any) => {
let rendered = this.renderText(props);
if (props.isBlock) {
const style = getStyleSheet(this.props.theme);
rendered = (
<View style={style.block}>
{rendered}
</View>
);
}
return rendered;
};
renderTable = ({children, numColumns}: {children: ReactElement; numColumns: number}) => {
return (
<MarkdownTable
numColumns={numColumns}
theme={this.props.theme}
>
{children}
</MarkdownTable>
);
};
renderTableRow = (args: MarkdownTableRowProps) => {
return <MarkdownTableRow {...args}/>;
};
renderTableCell = (args: MarkdownTableCellProps) => {
return <MarkdownTableCell {...args}/>;
};
renderLink = ({children, href}: {children: ReactElement; href: string}) => {
return (
<MarkdownLink href={href}>
{children}
</MarkdownLink>
);
};
renderEditedIndicator = ({context}: {context: string[]}) => {
let spacer = '';
if (context[0] === 'paragraph') {
spacer = ' ';
}
const style = getStyleSheet(this.props.theme);
const styles = [
this.props.baseTextStyle,
style.editedIndicatorText,
];
return (
<Text
style={styles}
>
{spacer}
<FormattedText
id='post_message_view.edited'
defaultMessage='(edited)'
/>
</Text>
);
};
render() {
let ast = this.parser.parse(this.props.value.toString());
ast = combineTextNodes(ast);
ast = addListItemIndices(ast);
ast = pullOutImages(ast);
if (this.props.mentionKeys) {
ast = highlightMentions(ast, this.props.mentionKeys);
}
if (this.props.isEdited) {
const editIndicatorNode = new Node('edited_indicator');
if (ast.lastChild && ['heading', 'paragraph'].includes(ast.lastChild.type)) {
ast.appendChild(editIndicatorNode);
} else {
const node = new Node('paragraph');
node.appendChild(editIndicatorNode);
ast.appendChild(node);
}
}
return this.renderer.render(ast);
}
}
export default Markdown;

View File

@@ -0,0 +1,503 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Parser, Node} from 'commonmark';
import Renderer from 'commonmark-react-renderer';
import React, {ReactElement, useRef} from 'react';
import {Dimensions, GestureResponderEvent, Platform, StyleProp, Text, TextStyle, View} from 'react-native';
import Emoji from '@components/emoji';
import FormattedText from '@components/formatted_text';
import Hashtag from '@components/markdown/hashtag';
import {blendColors, concatStyles, makeStyleSheetFromTheme} from '@utils/theme';
import {getScheme} from '@utils/url';
import AtMention from './at_mention';
import ChannelMention, {ChannelMentions} from './channel_mention';
import MarkdownBlockQuote from './markdown_block_quote';
import MarkdownCodeBlock from './markdown_code_block';
import MarkdownImage from './markdown_image';
import MarkdownLatexCodeBlock from './markdown_latex_block';
import MarkdownLatexInline from './markdown_latex_inline';
import MarkdownLink from './markdown_link';
import MarkdownList from './markdown_list';
import MarkdownListItem from './markdown_list_item';
import MarkdownTable from './markdown_table';
import MarkdownTableCell, {MarkdownTableCellProps} from './markdown_table_cell';
import MarkdownTableImage from './markdown_table_image';
import MarkdownTableRow, {MarkdownTableRowProps} from './markdown_table_row';
import {addListItemIndices, combineTextNodes, highlightMentions, pullOutImages} from './transform';
import type {
MarkdownAtMentionRenderer, MarkdownBaseRenderer, MarkdownBlockStyles, MarkdownChannelMentionRenderer,
MarkdownEmojiRenderer, MarkdownImageRenderer, MarkdownLatexRenderer, MarkdownTextStyles, UserMentionKey,
} from '@typings/global/markdown';
type MarkdownProps = {
autolinkedUrlSchemes?: string[];
baseTextStyle: StyleProp<TextStyle>;
blockStyles?: MarkdownBlockStyles;
channelMentions?: ChannelMentions;
disableAtChannelMentionHighlight?: boolean;
disableAtMentions?: boolean;
disableChannelLink?: boolean;
disableGallery?: boolean;
disableHashtags?: boolean;
enableLatex: boolean;
enableInlineLatex: boolean;
imagesMetadata?: Record<string, PostImage>;
isEdited?: boolean;
isReplyPost?: boolean;
isSearchResult?: boolean;
layoutWidth?: number;
location?: string;
mentionKeys?: UserMentionKey[];
minimumHashtagLength?: number;
onPostPress?: (event: GestureResponderEvent) => void;
postId?: string;
textStyles?: MarkdownTextStyles;
theme: Theme;
value?: string | number;
}
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
// Android has trouble giving text transparency depending on how it's nested,
// so we calculate the resulting colour manually
const editedOpacity = Platform.select({
ios: 0.3,
android: 1.0,
});
const editedColor = Platform.select({
ios: theme.centerChannelColor,
android: blendColors(theme.centerChannelBg, theme.centerChannelColor, 0.3),
});
return {
block: {
alignItems: 'flex-start',
flexDirection: 'row',
flexWrap: 'wrap',
},
editedIndicatorText: {
color: editedColor,
opacity: editedOpacity,
},
atMentionOpacity: {
opacity: 1,
},
};
});
const getExtraPropsForNode = (node: any) => {
const extraProps: Record<string, any> = {
continue: node.continue,
index: node.index,
};
if (node.type === 'image') {
extraProps.reactChildren = node.react.children;
extraProps.linkDestination = node.linkDestination;
extraProps.size = node.size;
}
return extraProps;
};
const computeTextStyle = (textStyles: MarkdownTextStyles, baseStyle: StyleProp<TextStyle>, context: any) => {
type TextType = keyof typeof textStyles;
const contextStyles: TextStyle[] = context.map((type: any) => textStyles[type as TextType]).filter((f: any) => f !== undefined);
return contextStyles.length ? concatStyles(baseStyle, contextStyles) : baseStyle;
};
const Markdown = ({
autolinkedUrlSchemes, baseTextStyle, blockStyles, channelMentions,
disableAtChannelMentionHighlight = false, disableAtMentions = false, disableChannelLink = false,
disableGallery = false, disableHashtags = false, enableInlineLatex, enableLatex,
imagesMetadata, isEdited, isReplyPost, isSearchResult, layoutWidth,
location, mentionKeys, minimumHashtagLength = 3, onPostPress, postId,
textStyles = {}, theme, value = '',
}: MarkdownProps) => {
const style = getStyleSheet(theme);
const urlFilter = (url: string) => {
const scheme = getScheme(url);
return !scheme || autolinkedUrlSchemes?.indexOf(scheme) !== -1;
};
const renderAtMention = ({context, mentionName}: MarkdownAtMentionRenderer) => {
if (disableAtMentions) {
return renderText({context, literal: `@${mentionName}`});
}
return (
<AtMention
disableAtChannelMentionHighlight={disableAtChannelMentionHighlight}
mentionStyle={textStyles.mention}
textStyle={[computeTextStyle(textStyles, baseTextStyle, context), style.atMentionOpacity]}
isSearchResult={isSearchResult}
mentionName={mentionName}
onPostPress={onPostPress}
mentionKeys={mentionKeys}
/>
);
};
const renderBlockQuote = ({children, ...otherProps}: any) => {
return (
<MarkdownBlockQuote
iconStyle={blockStyles?.quoteBlockIcon}
{...otherProps}
>
{children}
</MarkdownBlockQuote>
);
};
const renderBreak = () => {
return <Text>{'\n'}</Text>;
};
const renderChannelLink = ({context, channelName}: MarkdownChannelMentionRenderer) => {
if (disableChannelLink) {
return renderText({context, literal: `~${channelName}`});
}
return (
<ChannelMention
linkStyle={textStyles.link}
textStyle={computeTextStyle(textStyles, baseTextStyle, context)}
channelName={channelName}
channelMentions={channelMentions}
/>
);
};
const renderCodeBlock = (props: any) => {
// These sometimes include a trailing newline
const content = props.literal.replace(/\n$/, '');
if (enableLatex && props.language === 'latex') {
return (
<MarkdownLatexCodeBlock
content={content}
theme={theme}
/>
);
}
return (
<MarkdownCodeBlock
content={content}
language={props.language}
textStyle={textStyles.codeBlock}
/>
);
};
const renderCodeSpan = ({context, literal}: MarkdownBaseRenderer) => {
const {code} = textStyles;
return <Text style={computeTextStyle(textStyles, [baseTextStyle, code], context)}>{literal}</Text>;
};
const renderEditedIndicator = ({context}: {context: string[]}) => {
let spacer = '';
const styles = [baseTextStyle, style.editedIndicatorText];
if (context[0] === 'paragraph') {
spacer = ' ';
}
return (
<Text
style={styles}
>
{spacer}
<FormattedText
id='post_message_view.edited'
defaultMessage='(edited)'
/>
</Text>
);
};
const renderEmoji = ({context, emojiName, literal}: MarkdownEmojiRenderer) => {
return (
<Emoji
emojiName={emojiName}
literal={literal}
testID='markdown_emoji'
textStyle={computeTextStyle(textStyles, baseTextStyle, context)}
/>
);
};
const renderHashtag = ({context, hashtag}: {context: string[]; hashtag: string}) => {
if (disableHashtags) {
return renderText({context, literal: `#${hashtag}`});
}
return (
<Hashtag
hashtag={hashtag}
linkStyle={textStyles.link}
/>
);
};
const renderHeading = ({children, level}: {children: ReactElement; level: string}) => {
const containerStyle = [
style.block,
textStyles[`heading${level}`],
];
const textStyle = textStyles[`heading${level}Text`];
return (
<View style={containerStyle}>
<Text style={textStyle}>
{children}
</Text>
</View>
);
};
const renderHtml = (props: any) => {
let rendered = renderText(props);
if (props.isBlock) {
rendered = (
<View style={style.block}>
{rendered}
</View>
);
}
return rendered;
};
const renderImage = ({linkDestination, context, src, size}: MarkdownImageRenderer) => {
if (!imagesMetadata) {
return null;
}
if (context.indexOf('table') !== -1) {
// We have enough problems rendering images as is, so just render a link inside of a table
return (
<MarkdownTableImage
disabled={disableGallery ?? Boolean(!location)}
imagesMetadata={imagesMetadata}
location={location}
postId={postId!}
source={src}
/>
);
}
return (
<MarkdownImage
disabled={disableGallery ?? Boolean(!location)}
errorTextStyle={[computeTextStyle(textStyles, baseTextStyle, context), textStyles.error]}
layoutWidth={layoutWidth}
linkDestination={linkDestination}
imagesMetadata={imagesMetadata}
isReplyPost={isReplyPost}
location={location}
postId={postId!}
source={src}
sourceSize={size}
/>
);
};
const renderLatexInline = ({context, latexCode}: MarkdownLatexRenderer) => {
if (!enableInlineLatex) {
return renderText({context, literal: `$${latexCode}$`});
}
return (
<Text>
<MarkdownLatexInline
content={latexCode}
maxMathWidth={Dimensions.get('window').width * 0.75}
theme={theme}
/>
</Text>
);
};
const renderLink = ({children, href}: {children: ReactElement; href: string}) => {
return (
<MarkdownLink href={href}>
{children}
</MarkdownLink>
);
};
const renderList = ({children, start, tight, type}: any) => {
return (
<MarkdownList
ordered={type !== 'bullet'}
start={start}
tight={tight}
>
{children}
</MarkdownList>
);
};
const renderListItem = ({children, context, ...otherProps}: any) => {
const level = context.filter((type: string) => type === 'list').length;
return (
<MarkdownListItem
bulletStyle={baseTextStyle}
level={level}
{...otherProps}
>
{children}
</MarkdownListItem>
);
};
const renderParagraph = ({children, first}: {children: ReactElement[]; first: boolean}) => {
if (!children || children.length === 0) {
return null;
}
const blockStyle = [style.block];
if (!first) {
blockStyle.push(blockStyles?.adjacentParagraph);
}
return (
<View style={blockStyle}>
<Text>
{children}
</Text>
</View>
);
};
const renderTable = ({children, numColumns}: {children: ReactElement; numColumns: number}) => {
return (
<MarkdownTable
numColumns={numColumns}
theme={theme}
>
{children}
</MarkdownTable>
);
};
const renderTableCell = (args: MarkdownTableCellProps) => {
return <MarkdownTableCell {...args}/>;
};
const renderTableRow = (args: MarkdownTableRowProps) => {
return <MarkdownTableRow {...args}/>;
};
const renderText = ({context, literal}: MarkdownBaseRenderer) => {
if (context.indexOf('image') !== -1) {
// If this text is displayed, it will be styled by the image component
return (
<Text testID='markdown_text'>
{literal}
</Text>
);
}
// Construct the text style based off of the parents of this node since RN's inheritance is limited
const styles = computeTextStyle(textStyles, baseTextStyle, context);
return (
<Text
testID='markdown_text'
style={styles}
>
{literal}
</Text>
);
};
const renderThematicBreak = () => {
return (
<View
style={blockStyles?.horizontalRule}
testID='markdown_thematic_break'
/>
);
};
const createRenderer = () => {
const renderers: any = {
text: renderText,
emph: Renderer.forwardChildren,
strong: Renderer.forwardChildren,
del: Renderer.forwardChildren,
code: renderCodeSpan,
link: renderLink,
image: renderImage,
atMention: renderAtMention,
channelLink: renderChannelLink,
emoji: renderEmoji,
hashtag: renderHashtag,
latexinline: renderLatexInline,
paragraph: renderParagraph,
heading: renderHeading,
codeBlock: renderCodeBlock,
blockQuote: renderBlockQuote,
list: renderList,
item: renderListItem,
hardBreak: renderBreak,
thematicBreak: renderThematicBreak,
softBreak: renderBreak,
htmlBlock: renderHtml,
htmlInline: renderHtml,
table: renderTable,
table_row: renderTableRow,
table_cell: renderTableCell,
mention_highlight: Renderer.forwardChildren,
editedIndicator: renderEditedIndicator,
};
return new Renderer({
renderers,
renderParagraphsInLists: true,
getExtraPropsForNode,
});
};
const parser = useRef(new Parser({urlFilter, minimumHashtagLength})).current;
const renderer = useRef(createRenderer()).current;
let ast = parser.parse(value.toString());
ast = combineTextNodes(ast);
ast = addListItemIndices(ast);
ast = pullOutImages(ast);
if (mentionKeys) {
ast = highlightMentions(ast, mentionKeys);
}
if (isEdited) {
const editIndicatorNode = new Node('edited_indicator');
if (ast.lastChild && ['heading', 'paragraph'].includes(ast.lastChild.type)) {
ast.appendChild(editIndicatorNode);
} else {
const node = new Node('paragraph');
node.appendChild(editIndicatorNode);
ast.appendChild(node);
}
}
return renderer.render(ast) as JSX.Element;
};
export default Markdown;

View File

@@ -71,7 +71,7 @@ const MarkdownCodeBlock = ({language = '', content, textStyle}: MarkdownCodeBloc
const insets = useSafeAreaInsets();
const style = getStyleSheet(theme);
const handlePress = preventDoubleTap(() => {
const handlePress = useCallback(preventDoubleTap(() => {
const screen = Screens.CODE;
const passProps = {
code: content,
@@ -102,7 +102,7 @@ const MarkdownCodeBlock = ({language = '', content, textStyle}: MarkdownCodeBloc
requestAnimationFrame(() => {
goToScreen(screen, title, passProps);
});
});
}), [content, intl.locale, language]);
const handleLongPress = useCallback(() => {
if (managedConfig?.copyAndPasteProtection !== 'true') {

View File

@@ -81,13 +81,7 @@ const MarkdownImage = ({
const originalSize = getMarkdownImageSize(isReplyPost, isTablet, sourceSize, metadata, layoutWidth);
const serverUrl = useServerUrl();
const galleryIdentifier = `${postId}-${genericFileId}-${location}`;
const uri = useMemo(() => {
if (source.startsWith('/')) {
return serverUrl + source;
}
return source;
}, [source, serverUrl]);
const uri = source.startsWith('/') ? serverUrl + source : source;
const fileInfo = useMemo(() => {
const link = decodeURIComponent(uri);
@@ -119,7 +113,7 @@ const MarkdownImage = ({
type: 'image',
};
openGalleryAtIndex(galleryIdentifier, 0, [item]);
}, []);
}, [fileInfo]);
const {ref, onGestureEvent, styles} = useGalleryItem(
galleryIdentifier,
@@ -188,7 +182,7 @@ const MarkdownImage = ({
theme,
});
}
}, [managedConfig, intl, insets, theme]);
}, [managedConfig, intl.locale, insets.bottom, theme]);
const handleOnError = useCallback(() => {
setFailed(true);

View File

@@ -0,0 +1,222 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {useManagedConfig} from '@mattermost/react-native-emm';
import Clipboard from '@react-native-community/clipboard';
import React, {useCallback, useMemo} from 'react';
import {useIntl} from 'react-intl';
import {Keyboard, View, Text, StyleSheet, Platform} from 'react-native';
import MathView from 'react-native-math-view';
import {useSafeAreaInsets} from 'react-native-safe-area-context';
import FormattedText from '@components/formatted_text';
import SlideUpPanelItem, {ITEM_HEIGHT} from '@components/slide_up_panel_item';
import TouchableWithFeedback from '@components/touchable_with_feedback';
import {Screens} from '@constants';
import {bottomSheet, dismissBottomSheet, goToScreen} from '@screens/navigation';
import {bottomSheetSnapPoint} from '@utils/helpers';
import {getHighlightLanguageName} from '@utils/markdown';
import {splitLatexCodeInLines} from '@utils/markdown/latex';
import {preventDoubleTap} from '@utils/tap';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import {typography} from '@utils/typography';
const MAX_LINES = 2;
type Props = {
content: string;
theme: Theme;
}
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
const codeVerticalPadding = Platform.select({
ios: 4,
android: 0,
});
return {
bottomSheet: {
flex: 1,
},
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),
...typography('Body', 50),
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,
...typography('Body', 75),
},
errorText: {
...typography('Body', 100),
marginHorizontal: 5,
color: theme.errorTextColor,
},
};
});
const LatexCodeBlock = ({content, theme}: Props) => {
const intl = useIntl();
const insets = useSafeAreaInsets();
const managedConfig = useManagedConfig<ManagedConfig>();
const styles = getStyleSheet(theme);
const languageDisplayName = getHighlightLanguageName('latex');
const split = useMemo(() => {
const lines = splitLatexCodeInLines(content);
const numberOfLines = lines.length;
if (numberOfLines > MAX_LINES) {
return {
content: lines.slice(0, MAX_LINES),
numberOfLines,
};
}
return {
lines,
numberOfLines,
};
}, [content]);
const handlePress = useCallback(preventDoubleTap(() => {
const screen = Screens.LATEX;
const passProps = {
content,
};
const title = intl.formatMessage({
id: 'mobile.routes.code',
defaultMessage: '{language} Code',
}, {
language: languageDisplayName,
});
Keyboard.dismiss();
requestAnimationFrame(() => {
goToScreen(screen, title, passProps);
});
}), [content, languageDisplayName, intl.locale]);
const handleLongPress = useCallback(() => {
if (managedConfig?.copyAndPasteProtection !== 'true') {
const renderContent = () => {
return (
<View
testID='at_mention.bottom_sheet'
style={styles.bottomSheet}
>
<SlideUpPanelItem
icon='content-copy'
onPress={() => {
dismissBottomSheet();
Clipboard.setString(content);
}}
testID='at_mention.bottom_sheet.copy_code'
text={intl.formatMessage({id: 'mobile.markdown.code.copy_code', defaultMessage: 'Copy Code'})}
/>
<SlideUpPanelItem
destructive={true}
icon='cancel'
onPress={dismissBottomSheet}
testID='at_mention.bottom_sheet.cancel'
text={intl.formatMessage({id: 'mobile.post.cancel', defaultMessage: 'Cancel'})}
/>
</View>
);
};
bottomSheet({
closeButtonId: 'close-code-block',
renderContent,
snapPoints: [bottomSheetSnapPoint(2, ITEM_HEIGHT, insets.bottom), 10],
title: intl.formatMessage({id: 'post.options.title', defaultMessage: 'Options'}),
theme,
});
}
}, [managedConfig?.copyAndPasteProtection, intl, insets, theme]);
const onRenderErrorMessage = useCallback(({error}: {error: Error}) => {
return <Text style={styles.errorText}>{'Render error: ' + error.message}</Text>;
}, []);
let plusMoreLines = null;
if (split.numberOfLines > MAX_LINES) {
plusMoreLines = (
<FormattedText
style={styles.plusMoreLinesText}
id='mobile.markdown.code.plusMoreLines'
defaultMessage='+{count, number} more {count, plural, one {line} other {lines}}'
values={{
count: split.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={handlePress}
onLongPress={handleLongPress}
type={'opacity'}
>
<View style={styles.container}>
<View style={styles.rightColumn}>
{split.lines?.map((latexCode) => (
<View
style={styles.code}
key={latexCode}
>
<MathView
math={latexCode}
renderError={onRenderErrorMessage}
resizeMode={'cover'}
/>
</View>
))}
{plusMoreLines}
</View>
<View style={styles.language}>
<Text style={styles.languageText}>
{languageDisplayName}
</Text>
</View>
</View>
</TouchableWithFeedback>
);
};
export default LatexCodeBlock;

View File

@@ -0,0 +1,59 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {Platform, Text, View} from 'react-native';
import MathView from 'react-native-math-view';
import {makeStyleSheetFromTheme} from '@utils/theme';
type Props = {
content: string;
maxMathWidth: number | string;
theme: Theme;
}
type MathViewErrorProps = {
error: Error;
}
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
return {
mathStyle: {
marginBottom: Platform.select({default: -10, ios: 2.5}),
},
viewStyle: {
flexDirection: 'row',
flexWrap: 'wrap',
},
errorText: {
flexDirection: 'row',
color: theme.errorTextColor,
flexWrap: 'wrap',
},
};
});
const LatexInline = ({content, maxMathWidth, theme}: Props) => {
const style = getStyleSheet(theme);
const onRenderErrorMessage = (errorMsg: MathViewErrorProps) => {
return <Text style={style.errorText}>{'Latex render error: ' + errorMsg.error.message}</Text>;
};
return (
<View
style={style.viewStyle}
key={content}
>
<MathView
style={[style.mathStyle, {maxWidth: maxMathWidth || '100%'}]}
math={content}
renderError={onRenderErrorMessage}
resizeMode='contain'
/>
</View>
);
};
export default LatexInline;

View File

@@ -36,6 +36,19 @@ const style = StyleSheet.create({
},
});
const parseLinkLiteral = (literal: string) => {
let nextLiteral = literal;
const WWW_REGEX = /\b^(?:www.)/i;
if (nextLiteral.match(WWW_REGEX)) {
nextLiteral = literal.replace(WWW_REGEX, 'www.');
}
const parsed = urlParse(nextLiteral, {});
return parsed.href;
};
const MarkdownLink = ({children, experimentalNormalizeMarkdownLinks, href, siteURL}: MarkdownLinkProps) => {
const intl = useIntl();
const insets = useSafeAreaInsets();
@@ -45,7 +58,7 @@ const MarkdownLink = ({children, experimentalNormalizeMarkdownLinks, href, siteU
const {formatMessage} = intl;
const handlePress = preventDoubleTap(async () => {
const handlePress = useCallback(preventDoubleTap(async () => {
const url = normalizeProtocol(href);
if (!url) {
@@ -80,22 +93,9 @@ const MarkdownLink = ({children, experimentalNormalizeMarkdownLinks, href, siteU
tryOpenURL(url, onError);
}
});
}), [href, intl.locale, serverUrl, siteURL]);
const parseLinkLiteral = (literal: string) => {
let nextLiteral = literal;
const WWW_REGEX = /\b^(?:www.)/i;
if (nextLiteral.match(WWW_REGEX)) {
nextLiteral = literal.replace(WWW_REGEX, 'www.');
}
const parsed = urlParse(nextLiteral, {});
return parsed.href;
};
const parseChildren = () => {
const parseChildren = useCallback(() => {
return Children.map(children, (child: ReactElement) => {
if (!child.props.literal || typeof child.props.literal !== 'string' || (child.props.context && child.props.context.length && !child.props.context.includes('link'))) {
return child;
@@ -115,7 +115,7 @@ const MarkdownLink = ({children, experimentalNormalizeMarkdownLinks, href, siteU
...otherChildProps,
};
});
};
}, [children]);
const handleLongPress = useCallback(() => {
if (managedConfig?.copyAndPasteProtection !== 'true') {

View File

@@ -86,7 +86,7 @@ const MarkTableImage = ({disabled, imagesMetadata, location, postId, serverURL,
type: 'image',
};
openGalleryAtIndex(galleryIdentifier, 0, [item]);
}, []);
}, [metadata, source, serverURL, currentServerUrl, postId]);
const {ref, onGestureEvent, styles} = useGalleryItem(
galleryIdentifier,

View File

@@ -27,6 +27,7 @@ export const HOME = 'Home';
export const INTEGRATION_SELECTOR = 'IntegrationSelector';
export const INTERACTIVE_DIALOG = 'InteractiveDialog';
export const IN_APP_NOTIFICATION = 'InAppNotification';
export const LATEX = 'Latex';
export const LOGIN = 'Login';
export const MENTIONS = 'Mentions';
export const MFA = 'MFA';
@@ -71,6 +72,7 @@ export default {
INTEGRATION_SELECTOR,
INTERACTIVE_DIALOG,
IN_APP_NOTIFICATION,
LATEX,
LOGIN,
MENTIONS,
MFA,

View File

@@ -132,6 +132,9 @@ Navigation.setLazyComponentRegistrator((screenName) => {
);
return;
}
case Screens.LATEX:
screen = withServerDatabase(require('@screens/latex').default);
break;
case Screens.LOGIN:
screen = withIntl(require('@screens/login').default);
break;

View File

@@ -0,0 +1,99 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {Platform, ScrollView, Text, View} from 'react-native';
import MathView from 'react-native-math-view';
import {SafeAreaView, Edge} from 'react-native-safe-area-context';
import {useTheme} from '@context/theme';
import {splitLatexCodeInLines} from '@utils/markdown/latex';
import {makeStyleSheetFromTheme} from '@utils/theme';
import {typography} from '@utils/typography';
type Props = {
content: string;
}
const edges: Edge[] = ['left', 'right'];
const getStyleSheet = makeStyleSheetFromTheme((theme: 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: {
...typography('Body', 100),
marginHorizontal: 5,
color: theme.errorTextColor,
},
};
});
const Latex = ({content}: Props) => {
const theme = useTheme();
const style = getStyleSheet(theme);
const lines = splitLatexCodeInLines(content);
const onErrorMessage = (errorMsg: Error) => {
return <Text style={style.errorText}>{'Error: ' + errorMsg.message}</Text>;
};
const onRenderErrorMessage = ({error}: {error: Error}) => {
return <Text style={style.errorText}>{'Render error: ' + error.message}</Text>;
};
return (
<SafeAreaView
edges={edges}
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={onErrorMessage}
renderError={onRenderErrorMessage}
resizeMode={'cover'}
/>
</View>
))}
</ScrollView>
</ScrollView>
</SafeAreaView>
);
};
export default Latex;

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

View File

@@ -0,0 +1,97 @@
// 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

@@ -747,6 +747,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 = (
@@ -768,6 +769,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

@@ -78,6 +78,7 @@ PODS:
- glog (0.3.5)
- hermes-engine (0.11.0)
- HMSegmentedControl (1.5.6)
- iosMath (0.9.4)
- jail-monkey (2.6.0):
- React-Core
- libevent (2.1.12)
@@ -469,6 +470,8 @@ PODS:
- React-Core
- RNLocalize (2.2.1):
- React-Core
- RNMathView (1.0.0):
- iosMath
- RNPermissions (3.3.1):
- React-Core
- RNReactNativeHapticFeedback (1.13.1):
@@ -632,6 +635,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`)
@@ -664,6 +668,7 @@ SPEC REPOS:
- fmt
- hermes-engine
- HMSegmentedControl
- iosMath
- libevent
- libwebp
- OpenSSL-Universal
@@ -811,6 +816,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:
@@ -865,6 +872,7 @@ SPEC CHECKSUMS:
glog: 476ee3e89abb49e07f822b48323c51c57124b572
hermes-engine: 84e3af1ea01dd7351ac5d8689cbbea1f9903ffc3
HMSegmentedControl: 34c1f54d822d8308e7b24f5d901ec674dfa31352
iosMath: f7a6cbadf9d836d2149c2a84c435b1effc244cba
jail-monkey: 07b83767601a373db876e939b8dbf3f5eb15f073
libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913
libwebp: 98a37e597e40bfdb4c911fc98f2c53d0b12d05fc
@@ -928,6 +936,7 @@ SPEC CHECKSUMS:
RNGestureHandler: 6e757e487a4834e7280e98e9bac66d2d9c575e9c
RNKeychain: 4f63aada75ebafd26f4bc2c670199461eab85d94
RNLocalize: cbcb55d0e19c78086ea4eea20e03fe8000bbbced
RNMathView: 4c8a3c081fa671ab3136c51fa0bdca7ffb708bd5
RNPermissions: 34d678157c800b25b22a488e4d8babb57456e796
RNReactNativeHapticFeedback: 4085973f5a38b40d3c6793a3ee5724773eae045e
RNReanimated: 89a32ebf01d2dac2eb35b3b1628952f626db93d7

View File

@@ -20,6 +20,6 @@ module.exports = {
'<rootDir>/dist/assets/images/video_player/$1@2x.png',
},
transformIgnorePatterns: [
'node_modules/(?!(@react-native|react-native)|jail-monkey|@sentry/react-native|@react-native-community/cameraroll|react-clone-referenced-element|@react-native-community|react-navigation|@react-navigation/.*|validator|react-syntax-highlighter/.*)',
'node_modules/(?!(@react-native|react-native)|jail-monkey|@sentry/react-native|@react-native-community/cameraroll|react-clone-referenced-element|@react-native-community|react-navigation|@react-navigation/.*|validator|react-syntax-highlighter/.*|hast-util-from-selector|hastscript|property-information|hast-util-parse-selector|space-separated-tokens|comma-separated-tokens|zwitch)',
],
};

285
package-lock.json generated
View File

@@ -71,6 +71,7 @@
"react-native-keychain": "8.0.0",
"react-native-linear-gradient": "2.5.6",
"react-native-localize": "2.2.1",
"react-native-math-view": "3.9.5",
"react-native-navigation": "7.26.0",
"react-native-neomorph-shadows": "1.1.2",
"react-native-notifications": "4.2.4",
@@ -8896,6 +8897,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",
@@ -11988,6 +11994,76 @@
"minimalistic-assert": "^1.0.1"
}
},
"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-from-selector/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/hast-util-from-selector/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/hast-util-from-selector/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/hast-util-from-selector/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/hast-util-from-selector/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/hast-util-parse-selector": {
"version": "2.2.5",
"resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-2.2.5.tgz",
@@ -15806,6 +15882,17 @@
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
"integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ="
},
"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/md5.js": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz",
@@ -16752,6 +16839,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",
@@ -16886,6 +16978,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",
@@ -19244,6 +19341,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-navigation": {
"version": "7.26.0",
"resolved": "https://registry.npmjs.org/react-native-navigation/-/react-native-navigation-7.26.0.tgz",
@@ -20832,6 +20944,27 @@
"url": "https://github.com/sponsors/wooorm"
}
},
"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.2.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-9.2.0.tgz",
"integrity": "sha512-e2i4wANQiSXgnrBlIatyHtP1odfUp0BbV5Y5nEGbxtIrStkEOAAzCUirvLBNXHLr7kwLvJl6V+4V3XV9x7Wd9w==",
"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",
@@ -21712,6 +21845,14 @@
"node": ">=8"
}
},
"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/traverse": {
"version": "0.3.9",
"resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz",
@@ -23147,6 +23288,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",
@@ -23357,6 +23503,14 @@
"node": ">=10.0.0"
}
},
"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",
@@ -23504,6 +23658,15 @@
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
},
"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": {
@@ -30100,6 +30263,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",
@@ -32482,6 +32650,54 @@
"minimalistic-assert": "^1.0.1"
}
},
"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"
},
"dependencies": {
"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=="
},
"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"
}
},
"property-information": {
"version": "6.1.1",
"resolved": "https://registry.npmjs.org/property-information/-/property-information-6.1.1.tgz",
"integrity": "sha512-hrzC564QIl0r0vy4l6MvRLhafmUowhO/O3KgVSoXIbbA2Sz4j8HGpJc6T2cubRVwMwpdiG/vKGfhT4IixmKN9w=="
},
"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=="
}
}
},
"hast-util-parse-selector": {
"version": "2.2.5",
"resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-2.2.5.tgz",
@@ -35353,6 +35569,17 @@
}
}
},
"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"
}
},
"md5.js": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz",
@@ -36165,6 +36392,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",
@@ -36271,6 +36503,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",
@@ -38195,6 +38432,17 @@
"integrity": "sha512-BuPaQWvxLZG1NrCDGqgAnecDrNQu3LED9/Pyl4H2LwTMHcEngXpE5PfVntW2GiLumdr6nUOkWmMnh8PynZqrsw==",
"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-navigation": {
"version": "7.26.0",
"resolved": "https://registry.npmjs.org/react-native-navigation/-/react-native-navigation-7.26.0.tgz",
@@ -39371,6 +39619,23 @@
"resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-1.1.5.tgz",
"integrity": "sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA=="
},
"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.2.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-9.2.0.tgz",
"integrity": "sha512-e2i4wANQiSXgnrBlIatyHtP1odfUp0BbV5Y5nEGbxtIrStkEOAAzCUirvLBNXHLr7kwLvJl6V+4V3XV9x7Wd9w=="
}
}
},
"split-on-first": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz",
@@ -40071,6 +40336,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=="
},
"traverse": {
"version": "0.3.9",
"resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz",
@@ -41222,6 +41492,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",
@@ -41386,6 +41661,11 @@
"resolved": "https://registry.npmjs.org/xmldom/-/xmldom-0.6.0.tgz",
"integrity": "sha512-iAcin401y58LckRZ0TkI4k0VSM1Qg0KGSc3i8rU+xrxe19A/BN1zHyVSJY7uoutVlaTSzYyk/v5AmkewAP7jtg=="
},
"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",
@@ -41498,6 +41778,11 @@
"version": "3.14.2",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.14.2.tgz",
"integrity": "sha512-iF+wrtzz7fQfkmn60PG6XFxaWBhYYKzp2i+nv24WbLUWb2JjymdkHlzBwP0erpc78WotwP5g9AAu7Sk8GWVVNw=="
},
"zwitch": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.2.tgz",
"integrity": "sha512-JZxotl7SxAJH0j7dN4pxsTV6ZLXoLdGME+PsjkL/DaBrVryK9kTGq06GfKrwcSOqypP+fdXGoCHE36b99fWVoA=="
}
}
}

View File

@@ -69,6 +69,7 @@
"react-native-keychain": "8.0.0",
"react-native-linear-gradient": "2.5.6",
"react-native-localize": "2.2.1",
"react-native-math-view": "3.9.5",
"react-native-navigation": "7.26.0",
"react-native-neomorph-shadows": "1.1.2",
"react-native-notifications": "4.2.4",

View File

@@ -63,6 +63,7 @@ interface ClientConfig {
EnableGuestAccounts: string;
EnableIncomingWebhooks: string;
EnableLatex: string;
EnableInlineLatex: string;
EnableLdap: string;
EnableLinkPreviews: string;
EnableMarketplace: string;

View File

@@ -46,3 +46,7 @@ export type MarkdownImageRenderer = {
height?: number;
};
}
export type MarkdownLatexRenderer = MarkdownBaseRenderer & {
latexCode: string;
}

View File

@@ -0,0 +1,4 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
declare module 'react-native-math-view';