* MM-61148 Rewrite table parsing based off cmark-gfm
* MM-61148 Add better error handling to Markdown code
* Use logError instead of console.error
* Switch back to published release
* Update import paths
(cherry picked from commit 0efa409023)
Co-authored-by: Harrison Healey <harrisonmhealey@gmail.com>
This commit is contained in:
parent
6aa27d18e3
commit
4cdf36c771
6 changed files with 217 additions and 30 deletions
|
|
@ -27,7 +27,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
|||
},
|
||||
}));
|
||||
|
||||
class ErrorBoundary extends React.PureComponent<Props, State, any> {
|
||||
class ErrorBoundary extends React.PureComponent<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = {hasError: false};
|
||||
|
|
@ -38,7 +38,6 @@ class ErrorBoundary extends React.PureComponent<Props, State, any> {
|
|||
}
|
||||
|
||||
render() {
|
||||
// eslint-disable-next-line react/prop-types
|
||||
const {children, error, theme} = this.props;
|
||||
const {hasError} = this.state;
|
||||
const style = getStyleSheet(theme);
|
||||
|
|
|
|||
140
app/components/markdown/markdown.test.tsx
Normal file
140
app/components/markdown/markdown.test.tsx
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {screen} from '@testing-library/react-native';
|
||||
import * as CommonMark from 'commonmark';
|
||||
const {Node} = CommonMark;
|
||||
import React from 'react';
|
||||
|
||||
import {Preferences} from '@constants';
|
||||
import {renderWithIntl} from '@test/intl-test-helper';
|
||||
|
||||
import Markdown from './markdown';
|
||||
import * as Transforms from './transform';
|
||||
|
||||
const Parser = jest.requireActual('commonmark').Parser;
|
||||
|
||||
describe('Markdown', () => {
|
||||
const baseProps: React.ComponentProps<typeof Markdown> = {
|
||||
baseTextStyle: {},
|
||||
enableInlineLatex: true,
|
||||
enableLatex: true,
|
||||
location: 'somewhere?',
|
||||
maxNodes: 2000,
|
||||
theme: Preferences.THEMES.denim,
|
||||
};
|
||||
|
||||
describe('error handling', () => {
|
||||
test('should render Markdown normally', () => {
|
||||
renderWithIntl(
|
||||
<Markdown
|
||||
{...baseProps}
|
||||
value='This is a test'
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByText('This is a test')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should catch errors when parsing Markdown', () => {
|
||||
jest.spyOn(CommonMark, 'Parser').mockImplementation(() => {
|
||||
const parser = new Parser();
|
||||
parser.incorporateLine = () => {
|
||||
throw new Error('test error');
|
||||
};
|
||||
return parser;
|
||||
});
|
||||
|
||||
renderWithIntl(
|
||||
<Markdown
|
||||
{...baseProps}
|
||||
value='This is a test'
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.queryByText('This is a text')).not.toBeVisible();
|
||||
expect(screen.getByText('An error occurred while parsing this text')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should catch errors when parsing Markdown', () => {
|
||||
jest.spyOn(CommonMark, 'Parser').mockImplementation(() => {
|
||||
const parser = new Parser();
|
||||
parser.incorporateLine = () => {
|
||||
throw new Error('test error');
|
||||
};
|
||||
return parser;
|
||||
});
|
||||
|
||||
renderWithIntl(
|
||||
<Markdown
|
||||
{...baseProps}
|
||||
value='This is a test'
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.queryByText('This is a text')).not.toBeVisible();
|
||||
expect(screen.getByText('An error occurred while parsing this text')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should catch errors when transforming Markdown', () => {
|
||||
jest.spyOn(Transforms, 'highlightMentions').mockImplementation(() => {
|
||||
throw new Error('test error');
|
||||
});
|
||||
|
||||
renderWithIntl(
|
||||
<Markdown
|
||||
{...baseProps}
|
||||
value='This is a test'
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.queryByText('This is a text')).not.toBeVisible();
|
||||
expect(screen.getByText('An error occurred while parsing this text')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should catch errors when rendering Markdown', () => {
|
||||
jest.spyOn(CommonMark, 'Parser').mockImplementation(() => {
|
||||
const parser = new Parser();
|
||||
parser.parse = () => {
|
||||
// This replicates what was returned by the parser to cause MM-61148
|
||||
const document = new Node('document');
|
||||
|
||||
const paragraph = new Node('paragraph');
|
||||
(paragraph as any)._string_content = 'some text';
|
||||
|
||||
document.appendChild(new Node('table'));
|
||||
|
||||
return document;
|
||||
};
|
||||
return parser;
|
||||
});
|
||||
|
||||
renderWithIntl(
|
||||
<Markdown
|
||||
{...baseProps}
|
||||
value='This is a test'
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.queryByText('This is a text')).not.toBeVisible();
|
||||
expect(screen.getByText('An error occurred while rendering this text')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should catch errors when with Latex', () => {
|
||||
const value =
|
||||
'```latex\n' +
|
||||
'\\\\\\\\asdfasdfasdf\n' +
|
||||
'```\n';
|
||||
|
||||
renderWithIntl(
|
||||
<Markdown
|
||||
{...baseProps}
|
||||
value={value}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.queryByText('This is a text')).not.toBeVisible();
|
||||
expect(screen.getByText('An error occurred while rendering this text')).toBeVisible();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -10,8 +10,10 @@ import {Dimensions, type GestureResponderEvent, Platform, type StyleProp, StyleS
|
|||
import CompassIcon from '@components/compass_icon';
|
||||
import Emoji from '@components/emoji';
|
||||
import FormattedText from '@components/formatted_text';
|
||||
import {logError} from '@utils/log';
|
||||
import {computeTextStyle} from '@utils/markdown';
|
||||
import {blendColors, changeOpacity, concatStyles, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
import {getScheme} from '@utils/url';
|
||||
|
||||
import AtMention from './at_mention';
|
||||
|
|
@ -98,6 +100,10 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
|||
color: editedColor,
|
||||
opacity: editedOpacity,
|
||||
},
|
||||
errorMessage: {
|
||||
color: theme.errorTextColor,
|
||||
...typography('Body', 100),
|
||||
},
|
||||
maxNodesWarning: {
|
||||
color: theme.errorTextColor,
|
||||
},
|
||||
|
|
@ -606,34 +612,74 @@ const Markdown = ({
|
|||
|
||||
const parser = useRef(new Parser({urlFilter, minimumHashtagLength})).current;
|
||||
const renderer = useMemo(createRenderer, [theme, textStyles]);
|
||||
let ast = parser.parse(value.toString());
|
||||
|
||||
ast = combineTextNodes(ast);
|
||||
ast = addListItemIndices(ast);
|
||||
ast = pullOutImages(ast);
|
||||
ast = parseTaskLists(ast);
|
||||
if (mentionKeys) {
|
||||
ast = highlightMentions(ast, mentionKeys);
|
||||
}
|
||||
if (highlightKeys) {
|
||||
ast = highlightWithoutNotification(ast, highlightKeys);
|
||||
}
|
||||
if (searchPatterns) {
|
||||
ast = highlightSearchPatterns(ast, searchPatterns);
|
||||
}
|
||||
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);
|
||||
const errorLogged = useRef(false);
|
||||
|
||||
ast.appendChild(node);
|
||||
let ast;
|
||||
try {
|
||||
ast = parser.parse(value.toString());
|
||||
|
||||
ast = combineTextNodes(ast);
|
||||
ast = addListItemIndices(ast);
|
||||
ast = pullOutImages(ast);
|
||||
ast = parseTaskLists(ast);
|
||||
if (mentionKeys) {
|
||||
ast = highlightMentions(ast, mentionKeys);
|
||||
}
|
||||
if (highlightKeys) {
|
||||
ast = highlightWithoutNotification(ast, highlightKeys);
|
||||
}
|
||||
if (searchPatterns) {
|
||||
ast = highlightSearchPatterns(ast, searchPatterns);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
if (!errorLogged.current) {
|
||||
logError('An error occurred while parsing Markdown', e);
|
||||
|
||||
errorLogged.current = true;
|
||||
}
|
||||
|
||||
return (
|
||||
<FormattedText
|
||||
id='markdown.parse_error'
|
||||
defaultMessage='An error occurred while parsing this text'
|
||||
style={style.errorMessage}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return renderer.render(ast) as JSX.Element;
|
||||
let output;
|
||||
try {
|
||||
output = renderer.render(ast);
|
||||
} catch (e) {
|
||||
if (!errorLogged.current) {
|
||||
logError('An error occurred while rendering Markdown', e);
|
||||
|
||||
errorLogged.current = true;
|
||||
}
|
||||
|
||||
return (
|
||||
<FormattedText
|
||||
id='markdown.render_error'
|
||||
defaultMessage='An error occurred while rendering this text'
|
||||
style={style.errorMessage}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return output;
|
||||
};
|
||||
|
||||
export default Markdown;
|
||||
|
|
|
|||
|
|
@ -454,6 +454,8 @@
|
|||
"login.username": "Username",
|
||||
"markdown.latex.error": "Latex render error",
|
||||
"markdown.max_nodes.error": "This message is too long to by shown fully on a mobile device. Please view it on desktop or contact an admin to increase this limit.",
|
||||
"markdown.parse_error": "An error occurred while parsing this text",
|
||||
"markdown.render_error": "An error occurred while rendering this text",
|
||||
"mentions.empty.paragraph": "You'll see messages here when someone mentions you or uses terms you're monitoring.",
|
||||
"mentions.empty.title": "No Mentions yet",
|
||||
"mobile.about.appVersion": "App Version: {version} (Build {number})",
|
||||
|
|
|
|||
8
package-lock.json
generated
8
package-lock.json
generated
|
|
@ -42,7 +42,7 @@
|
|||
"@stream-io/flat-list-mvcp": "0.10.3",
|
||||
"@voximplant/react-native-foreground-service": "3.0.2",
|
||||
"base-64": "1.0.0",
|
||||
"commonmark": "npm:@mattermost/commonmark@0.30.1-3",
|
||||
"commonmark": "npm:@mattermost/commonmark@0.30.1-4",
|
||||
"commonmark-react-renderer": "github:mattermost/commonmark-react-renderer#81b5d27509652bae50b4b510ede777dd3bd923cf",
|
||||
"deep-equal": "2.2.3",
|
||||
"deepmerge": "4.3.1",
|
||||
|
|
@ -12057,9 +12057,9 @@
|
|||
},
|
||||
"node_modules/commonmark": {
|
||||
"name": "@mattermost/commonmark",
|
||||
"version": "0.30.1-3",
|
||||
"resolved": "https://registry.npmjs.org/@mattermost/commonmark/-/commonmark-0.30.1-3.tgz",
|
||||
"integrity": "sha512-Kjsl/sZmb6R6PtpPVIifPfqBMrKs7z6Tukb1TJl/S0LfC5uNici3yol4waGxhGsJuvCF2kZqStwVQP7ieUbAgw==",
|
||||
"version": "0.30.1-4",
|
||||
"resolved": "https://registry.npmjs.org/@mattermost/commonmark/-/commonmark-0.30.1-4.tgz",
|
||||
"integrity": "sha512-7O1xQfP3ILHf/+wjrxwF0rqowuvqQ7EY+SJOWsDaV+VXdj+lmft9yxkL0QPNYALinKFdPEj1gFtgR+pi4w8lzQ==",
|
||||
"dependencies": {
|
||||
"entities": "~3.0.1",
|
||||
"mdurl": "~1.0.1",
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@
|
|||
"@stream-io/flat-list-mvcp": "0.10.3",
|
||||
"@voximplant/react-native-foreground-service": "3.0.2",
|
||||
"base-64": "1.0.0",
|
||||
"commonmark": "npm:@mattermost/commonmark@0.30.1-3",
|
||||
"commonmark": "npm:@mattermost/commonmark@0.30.1-4",
|
||||
"commonmark-react-renderer": "github:mattermost/commonmark-react-renderer#81b5d27509652bae50b4b510ede777dd3bd923cf",
|
||||
"deep-equal": "2.2.3",
|
||||
"deepmerge": "4.3.1",
|
||||
|
|
|
|||
Loading…
Reference in a new issue