ICU-624 Added markdown tables (#1434)

* ICU-624 Added initial table rendering support

* ICU-624 Added full table view

* Added proper pluralization to +X more lines in code blocks

* ICU-624 Added support for images in tables

* Stopped using injectIntl for tables

* ICU-624 Updated commonmark-react-renderer

* ICU-624 Hid scroll indicator in collapsed table view

* Addressed feedback
This commit is contained in:
Harrison Healey 2018-02-16 17:31:14 -05:00 committed by enahum
parent 84a2a0fab8
commit ede4d22f75
21 changed files with 869 additions and 131 deletions

View file

@ -24,6 +24,10 @@ 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 MarkdownTableImage from './markdown_table_image';
import MarkdownTableRow from './markdown_table_row';
import MarkdownTableCell from './markdown_table_cell';
import {addListItemIndices, pullOutImages} from './transform';
export default class Markdown extends PureComponent {
@ -83,6 +87,10 @@ export default class Markdown extends PureComponent {
htmlBlock: this.renderHtml,
htmlInline: this.renderHtml,
table: this.renderTable,
table_row: MarkdownTableRow,
table_cell: MarkdownTableCell,
editedIndicator: this.renderEditedIndicator
},
renderParagraphsInLists: true,
@ -124,6 +132,21 @@ export default class Markdown extends PureComponent {
}
renderImage = ({linkDestination, reactChildren, context, src}) => {
if (context.indexOf('table') !== -1) {
// We have enough problems rendering images as is, so just render a link inside of a table
return (
<MarkdownTableImage
linkDestination={linkDestination}
onLongPress={this.props.onLongPress}
source={src}
textStyle={[this.computeTextStyle(this.props.baseTextStyle, context), this.props.textStyles.link]}
navigator={this.props.navigator}
>
{reactChildren}
</MarkdownTableImage>
);
}
return (
<MarkdownImage
linkDestination={linkDestination}
@ -284,6 +307,14 @@ export default class Markdown extends PureComponent {
return rendered;
}
renderTable = ({children}) => {
return (
<MarkdownTable navigator={this.props.navigator}>
{children}
</MarkdownTable>
);
}
renderLink = ({children, href}) => {
return (
<MarkdownLink
@ -337,7 +368,8 @@ export default class Markdown extends PureComponent {
ast.appendChild(node);
}
}
return <View>{this.renderer.render(ast)}</View>;
return this.renderer.render(ast);
}
}

View file

@ -145,7 +145,7 @@ class MarkdownCodeBlock extends React.PureComponent {
<FormattedText
style={style.plusMoreLinesText}
id='mobile.markdown.code.plusMoreLines'
defaultMessage='+{count, number} more lines'
defaultMessage='+{count, number} more {count, plural, one {line} other {lines}}'
values={{
count: numberOfLines - MAX_LINES
}}

View file

@ -0,0 +1,16 @@
// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {connect} from 'react-redux';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import MarkdownTable from './markdown_table';
function mapStateToProps(state) {
return {
theme: getTheme(state)
};
}
export default connect(mapStateToProps)(MarkdownTable);

View file

@ -0,0 +1,146 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {PropTypes} from 'prop-types';
import React from 'react';
import {intlShape} from 'react-intl';
import {
ScrollView,
TouchableOpacity,
View
} from 'react-native';
import LinearGradient from 'react-native-linear-gradient';
import {wrapWithPreventDoubleTap} from 'app/utils/tap';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
const MAX_HEIGHT = 120;
export default class MarkdownTable extends React.PureComponent {
static propTypes = {
children: PropTypes.node.isRequired,
navigator: PropTypes.object.isRequired,
theme: PropTypes.object.isRequired
};
static contextTypes = {
intl: intlShape.isRequired
};
constructor(props) {
super(props);
this.state = {
contentCutOff: true
};
}
handlePress = wrapWithPreventDoubleTap(() => {
const {navigator, theme} = this.props;
navigator.push({
screen: 'Table',
title: this.context.intl.formatMessage({
id: 'mobile.routes.table',
defaultMessage: 'Table'
}),
animated: true,
backButtonTitle: '',
passProps: {
renderRows: this.renderRows
},
navigatorStyle: {
navBarTextColor: theme.sidebarHeaderTextColor,
navBarBackgroundColor: theme.sidebarHeaderBg,
navBarButtonColor: theme.sidebarHeaderTextColor,
screenBackgroundColor: theme.centerChannelBg
}
});
});
handleContentHeightChange = (contentWidth, contentHeight) => {
this.setState({
contentCutOff: contentHeight >= MAX_HEIGHT
});
}
renderRows = (drawBottomBorder = true) => {
const style = getStyleSheet(this.props.theme);
const tableStyle = [style.table];
if (drawBottomBorder) {
tableStyle.push(style.tableBottomBorder);
}
// Add an extra prop to the last row of the table so that it knows not to render a bottom border
// since the container should be rendering that
const rows = React.Children.toArray(this.props.children);
rows[rows.length - 1] = React.cloneElement(rows[rows.length - 1], {
isLastRow: true
});
return (
<View style={tableStyle}>
{rows}
</View>
);
}
render() {
const style = getStyleSheet(this.props.theme);
let moreIndicator = null;
if (this.state.contentCutOff) {
moreIndicator = (
<LinearGradient
colors={[
changeOpacity(this.props.theme.centerChannelColor, 0.0),
changeOpacity(this.props.theme.centerChannelColor, 0.1)
]}
style={style.moreIndicator}
/>
);
}
return (
<TouchableOpacity onPress={this.handlePress}>
<ScrollView
onContentSizeChange={this.handleContentHeightChange}
style={style.container}
scrollEnabled={false}
showsVerticalScrollIndicator={false}
>
{this.renderRows(false)}
</ScrollView>
{moreIndicator}
</TouchableOpacity>
);
}
}
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return {
container: {
borderBottomWidth: 1,
borderColor: changeOpacity(theme.centerChannelColor, 0.2),
maxHeight: MAX_HEIGHT
},
table: {
borderColor: changeOpacity(theme.centerChannelColor, 0.2),
borderLeftWidth: 1, // The right border is drawn by the MarkdownTableCell component
borderTopWidth: 1,
flex: 1
},
tableBottomBorder: {
borderBottomWidth: 1,
borderColor: changeOpacity(theme.centerChannelColor, 0.2)
},
moreIndicator: {
bottom: 0,
height: 20,
position: 'absolute',
right: 0,
width: '100%'
}
};
});

View file

@ -0,0 +1,16 @@
// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {connect} from 'react-redux';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import MarkdownTableCell from './markdown_table_cell';
function mapStateToProps(state) {
return {
theme: getTheme(state)
};
}
export default connect(mapStateToProps)(MarkdownTableCell);

View file

@ -0,0 +1,46 @@
// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import PropTypes from 'prop-types';
import React from 'react';
import {Text, View} from 'react-native';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
export default class MarkdownTableCell extends React.PureComponent {
static propTypes = {
children: PropTypes.node,
isHeading: PropTypes.bool.isRequired,
theme: PropTypes.object.isRequired
};
render() {
const style = getStyleSheet(this.props.theme);
const cellStyle = [style.cell];
if (this.props.isHeading) {
cellStyle.push(style.heading);
}
return (
<View style={cellStyle}>
<Text>
{this.props.children}
</Text>
</View>
);
}
}
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return {
cell: {
borderColor: changeOpacity(theme.centerChannelColor, 0.2),
borderRightWidth: 1,
flex: 1,
justifyContent: 'flex-start',
paddingHorizontal: 13,
paddingVertical: 6
}
};
});

View file

@ -0,0 +1,18 @@
// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {connect} from 'react-redux';
import {getCurrentUrl} from 'mattermost-redux/selectors/entities/general';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import MarkdownTableImage from './markdown_table_image';
function mapStateToProps(state) {
return {
serverURL: getCurrentUrl(state),
theme: getTheme(state)
};
}
export default connect(mapStateToProps)(MarkdownTableImage);

View file

@ -0,0 +1,69 @@
// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {PropTypes} from 'prop-types';
import React from 'react';
import {intlShape} from 'react-intl';
import {Text} from 'react-native';
import CustomPropTypes from 'app/constants/custom_prop_types';
import {wrapWithPreventDoubleTap} from 'app/utils/tap';
export default class MarkdownTableImage extends React.PureComponent {
static propTypes = {
children: PropTypes.node.isRequired,
source: PropTypes.string.isRequired,
textStyle: CustomPropTypes.Style.isRequired,
navigator: PropTypes.object.isRequired,
serverURL: PropTypes.string.isRequired,
theme: PropTypes.object.isRequired
};
static contextTypes = {
intl: intlShape.isRequired
};
handlePress = wrapWithPreventDoubleTap(() => {
const {navigator, theme} = this.props;
navigator.push({
screen: 'TableImage',
title: this.context.intl.formatMessage({
id: 'mobile.routes.tableImage',
defaultMessage: 'Image'
}),
animated: true,
backButtonTitle: '',
passProps: {
imageSource: this.getImageSource()
},
navigatorStyle: {
navBarTextColor: theme.sidebarHeaderTextColor,
navBarBackgroundColor: theme.sidebarHeaderBg,
navBarButtonColor: theme.sidebarHeaderTextColor,
screenBackgroundColor: theme.centerChannelBg
}
});
});
getImageSource = () => {
let source = this.props.source;
if (source.startsWith('/')) {
source = `${this.props.serverURL}/${source}`;
}
return source;
};
render() {
return (
<Text
onPress={this.handlePress}
style={this.props.textStyle}
>
{this.props.children}
</Text>
);
}
}

View file

@ -0,0 +1,16 @@
// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {connect} from 'react-redux';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import MarkdownTableRow from './markdown_table_row';
function mapStateToProps(state) {
return {
theme: getTheme(state)
};
}
export default connect(mapStateToProps)(MarkdownTableRow);

View file

@ -0,0 +1,41 @@
// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import PropTypes from 'prop-types';
import React from 'react';
import {View} from 'react-native';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
export default class MarkdownTableRow extends React.PureComponent {
static propTypes = {
children: PropTypes.node,
isLastRow: PropTypes.bool,
theme: PropTypes.object.isRequired
};
render() {
const style = getStyleSheet(this.props.theme);
const rowStyle = [style.row];
if (!this.props.isLastRow) {
rowStyle.push(style.rowBottomBorder);
}
return <View style={rowStyle}>{this.props.children}</View>;
}
}
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return {
row: {
flex: 1,
flexDirection: 'row',
justifyContent: 'flex-start'
},
rowBottomBorder: {
borderColor: changeOpacity(theme.centerChannelColor, 0.2),
borderBottomWidth: 1
}
};
});

View file

@ -27,134 +27,24 @@ export function addListItemIndices(ast) {
return ast;
}
// Take all images and move them to be children of the root document node. When this happens, their
// parent nodes are split into two, if necessary, with the version that follows the image having its
// "continue" field set to true to indicate that things like bullet points don't need to be rendered.
// Take all images, that aren't inside of tables, and move them to be children of the root document node.
// When this happens, their parent nodes are split into two, if necessary, with the version that follows
// the image having its "continue" field set to true to indicate that things like bullet points don't
// need to be rendered.
export function pullOutImages(ast) {
for (let block = ast.firstChild; block !== null; block = block.next) {
// Skip tables because we'll render images inside of those as links
if (block.type === 'table') {
continue;
}
let node = block.firstChild;
let cameFromChild = false;
while (node && node !== block) {
if (node.type === 'image' && node.parent.type !== 'document') {
const image = node;
let parent = image.parent;
let prev = image.prev;
let next = image.next;
// Remove image from its siblings
if (prev) {
prev._next = next;
}
if (next) {
next._prev = prev;
// Since the following text will be on a new line, a preceding space would cause the
// alignment to be incorrect
if (next.type === 'text' && next.literal.startsWith(' ')) {
next.literal = next.literal.substring(1);
}
}
// And from its parents
if (parent._firstChild === image) {
// image was the first child (ie prev is null), so the next sibling is now the first child
parent._firstChild = next;
}
if (parent._lastChild === image) {
// image was the last child (ie next is null), so the previous sibling is now the last child
parent._lastChild = prev;
}
// Split the tree between the previous and next siblings, where the image would've been
while (parent && parent.type !== 'document') {
// We only need to split the parent if there's anything on the right of where we're splitting
// in the current branch
let parentCopy = null;
// Split if we have children to the right of the split (next) or if we have any siblings to the
// right of the parent (parent.next)
if (next) {
parentCopy = copyNodeWithoutNeighbors(parent);
// Set an additional flag so we know not to re-render things like bullet points
parentCopy.continue = true;
// Re-assign the children to the right of the split to belong to the copy
parentCopy._firstChild = next;
parentCopy._lastChild = getLastSibling(next);
if (parent._firstChild === next) {
parent._firstChild = null;
parent._lastChild = null;
} else {
parent._lastChild = prev;
}
// And re-assign the parent of all of those to be the copy
for (let child = parentCopy.firstChild; child; child = child.next) {
child._parent = parentCopy;
}
// Insert the copy as parent's next sibling
if (parent.next) {
parent.next._prev = parentCopy;
parentCopy._next = parent.next;
parent._next = parentCopy;
} else /* if (parent.parent.lastChild === parent) */ {
// Since parent has no next sibling, parent is the last child of its parent, so
// we need to set the copy as the last child
parent.parent.lastChild = parentCopy;
}
}
// Change prev and next to no longer be siblings
if (prev) {
prev._next = null;
}
if (next) {
next._prev = null;
}
// This image is part of a link so include the destination along with it
if (parent.type === 'link') {
image.linkDestination = parent.destination;
}
// Move up the tree
next = parentCopy || parent.next;
prev = parent;
parent = parent.parent;
}
// Re-insert the image now that we have a tree split down to the root with the image's ancestors.
// Note that parent is the root node, prev is the ancestor of image, and next is the ancestor of the copy
// Add image to its parent
image._parent = parent;
if (next) {
parent._lastChild = next;
} else {
// image is the last child of the root node now
parent._lastChild = image;
}
// Add image to its siblings
image._prev = prev;
prev._next = image;
image._next = next;
if (next) {
next._prev = image;
}
// The copy still needs its parent set to the root node
if (next) {
next._parent = parent;
}
pullOutImage(node);
}
// Walk through tree to next node
@ -174,6 +64,124 @@ export function pullOutImages(ast) {
return ast;
}
function pullOutImage(image) {
let parent = image.parent;
let prev = image.prev;
let next = image.next;
// Remove image from its siblings
if (prev) {
prev._next = next;
}
if (next) {
next._prev = prev;
// Since the following text will be on a new line, a preceding space would cause the
// alignment to be incorrect
if (next.type === 'text' && next.literal.startsWith(' ')) {
next.literal = next.literal.substring(1);
}
}
// And from its parents
if (parent._firstChild === image) {
// image was the first child (ie prev is null), so the next sibling is now the first child
parent._firstChild = next;
}
if (parent._lastChild === image) {
// image was the last child (ie next is null), so the previous sibling is now the last child
parent._lastChild = prev;
}
// Split the tree between the previous and next siblings, where the image would've been
while (parent && parent.type !== 'document') {
// We only need to split the parent if there's anything on the right of where we're splitting
// in the current branch
let parentCopy = null;
// Split if we have children to the right of the split (next) or if we have any siblings to the
// right of the parent (parent.next)
if (next) {
parentCopy = copyNodeWithoutNeighbors(parent);
// Set an additional flag so we know not to re-render things like bullet points
parentCopy.continue = true;
// Re-assign the children to the right of the split to belong to the copy
parentCopy._firstChild = next;
parentCopy._lastChild = getLastSibling(next);
if (parent._firstChild === next) {
parent._firstChild = null;
parent._lastChild = null;
} else {
parent._lastChild = prev;
}
// And re-assign the parent of all of those to be the copy
for (let child = parentCopy.firstChild; child; child = child.next) {
child._parent = parentCopy;
}
// Insert the copy as parent's next sibling
if (parent.next) {
parent.next._prev = parentCopy;
parentCopy._next = parent.next;
parent._next = parentCopy;
} else /* if (parent.parent.lastChild === parent) */ {
// Since parent has no next sibling, parent is the last child of its parent, so
// we need to set the copy as the last child
parent.parent.lastChild = parentCopy;
}
}
// Change prev and next to no longer be siblings
if (prev) {
prev._next = null;
}
if (next) {
next._prev = null;
}
// This image is part of a link so include the destination along with it
if (parent.type === 'link') {
image.linkDestination = parent.destination;
}
// Move up the tree
next = parentCopy || parent.next;
prev = parent;
parent = parent.parent;
}
// Re-insert the image now that we have a tree split down to the root with the image's ancestors.
// Note that parent is the root node, prev is the ancestor of image, and next is the ancestor of the copy
// Add image to its parent
image._parent = parent;
if (next) {
parent._lastChild = next;
} else {
// image is the last child of the root node now
parent._lastChild = image;
}
// Add image to its siblings
image._prev = prev;
prev._next = image;
image._next = next;
if (next) {
next._prev = image;
}
// The copy still needs its parent set to the root node
if (next) {
next._parent = parent;
}
}
// Copies a Node without its parent, children, or siblings
function copyNodeWithoutNeighbors(node) {
// commonmark uses classes so it takes a bit of work to copy them

View file

@ -40,6 +40,8 @@ import Search from 'app/screens/search';
import SelectServer from 'app/screens/select_server';
import SelectTeam from 'app/screens/select_team';
import Settings from 'app/screens/settings/general';
import Table from 'app/screens/table';
import TableImage from 'app/screens/table_image';
import Thread from 'app/screens/thread';
import UserProfile from 'app/screens/user_profile';
@ -95,6 +97,8 @@ export function registerScreens(store, Provider) {
Navigation.registerComponent('SelectTeam', () => wrapWithContextProvider(SelectTeam), store, Provider);
Navigation.registerComponent('Settings', () => wrapWithContextProvider(Settings), store, Provider);
Navigation.registerComponent('SSO', () => wrapWithContextProvider(SSO), store, Provider);
Navigation.registerComponent('Table', () => wrapWithContextProvider(Table), store, Provider);
Navigation.registerComponent('TableImage', () => wrapWithContextProvider(TableImage), store, Provider);
Navigation.registerComponent('Thread', () => wrapWithContextProvider(Thread), store, Provider);
Navigation.registerComponent('UserProfile', () => wrapWithContextProvider(UserProfile), store, Provider);
}

View file

@ -0,0 +1,6 @@
// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import Table from './table';
export default Table;

View file

@ -0,0 +1,32 @@
// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import PropTypes from 'prop-types';
import React from 'react';
import {ScrollView, StyleSheet} from 'react-native';
export default class Table extends React.PureComponent {
static propTypes = {
renderRows: PropTypes.func.isRequired
};
render() {
return (
<ScrollView
style={style.scrollContainer}
contentContainerStyle={style.container}
>
{this.props.renderRows()}
</ScrollView>
);
}
}
const style = StyleSheet.create({
scrollContainer: {
flex: 1
},
container: {
flexDirection: 'row'
}
});

View file

@ -0,0 +1,16 @@
// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {connect} from 'react-redux';
import {getDimensions} from 'app/selectors/device';
import TableImage from './table_image';
function mapStateToProps(state) {
return {
deviceWidth: getDimensions(state).deviceWidth
};
}
export default connect(mapStateToProps)(TableImage);

View file

@ -0,0 +1,102 @@
// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import PropTypes from 'prop-types';
import React from 'react';
import {
ActivityIndicator,
Image,
ScrollView,
StyleSheet,
View
} from 'react-native';
export default class TableImage extends React.PureComponent {
static propTypes = {
deviceWidth: PropTypes.number.isRequired,
imageSource: PropTypes.string.isRequired
};
constructor(props) {
super(props);
this.state = {
width: -1,
height: -1
};
}
componentWillMount() {
this.getImageSize(this.props.imageSource);
}
componentWillReceiveProps(nextProps) {
if (this.props.imageSource !== nextProps.imageSource) {
this.setState({
width: -1,
height: -1
});
this.getImageSize(nextProps.imageSource);
}
}
getImageSize = (imageSource) => {
Image.getSize(imageSource, (width, height) => {
this.setState({
width,
height
});
});
}
render() {
let {width, height} = this.state;
if (width === -1 || height === -1) {
return (
<View style={style.loadingContainer}>
<ActivityIndicator
animating={true}
size='large'
/>
</View>
);
}
if (width > this.props.deviceWidth) {
height = (height / width) * this.props.deviceWidth;
width = this.props.deviceWidth;
}
return (
<ScrollView
style={style.scrollContainer}
contentContainerStyle={style.container}
>
<Image
style={[style.image, {width, height}]}
source={{uri: this.props.imageSource}}
/>
</ScrollView>
);
}
}
const style = StyleSheet.create({
scrollContainer: {
flex: 1
},
container: {
flex: 1,
flexDirection: 'column'
},
image: {
resizeMode: 'contain'
},
loadingContainer: {
alignItems: 'center',
flex: 1,
justifyContent: 'center'
}
});

View file

@ -92,6 +92,9 @@ export const getMarkdownTextStyles = makeStyleSheetFromTheme((theme) => {
},
error: {
color: theme.errorTextColor
},
table_header_row: {
fontWeight: '700'
}
};
});

View file

@ -2058,7 +2058,7 @@
"mobile.managed.jailbreak": "Jailbroken devices are not trusted by {vendor}, please exit the app.",
"mobile.managed.secured_by": "Secured by {vendor}",
"mobile.markdown.code.copy_code": "Copy Code",
"mobile.markdown.code.plusMoreLines": "+{count, number} more lines",
"mobile.markdown.code.plusMoreLines": "+{count, number} more {count, plural, one {line} other {lines}}",
"mobile.markdown.image.error": "Image failed to load:",
"mobile.markdown.image.too_large": "Image exceeds max dimensions of {maxWidth} by {maxHeight}:",
"mobile.markdown.link.copy_url": "Copy URL",
@ -2147,6 +2147,8 @@
"mobile.routes.selectTeam": "Select Team",
"mobile.routes.settings": "Settings",
"mobile.routes.sso": "Single Sign-On",
"mobile.routes.table": "Table",
"mobile.routes.tableImage": "Image",
"mobile.routes.thread": "{channelName} Thread",
"mobile.routes.thread_dm": "Direct Message Thread",
"mobile.routes.user_profile": "Profile",

View file

@ -9,8 +9,8 @@
"dependencies": {
"analytics-react-native": "1.2.0",
"babel-polyfill": "6.26.0",
"commonmark": "hmhealey/commonmark.js#dda6d89198252d5fd4bb04c4cc0668766c22fd77",
"commonmark-react-renderer": "hmhealey/commonmark-react-renderer#1f078d53b7993d30b2762dd7d78fdd21ee84dd21",
"commonmark": "hmhealey/commonmark.js",
"commonmark-react-renderer": "hmhealey/commonmark-react-renderer",
"deep-equal": "1.0.1",
"fuse.js": "^3.2.0",
"intl": "1.2.5",

View file

@ -338,6 +338,7 @@ describe('Components.Markdown.transform', () => {
const actual = pullOutImages(input);
assert.ok(verifyAst(actual));
assert.equal(astToString(actual), astToString(expected));
assert.deepStrictEqual(actual, expected);
});
@ -348,6 +349,7 @@ describe('Components.Markdown.transform', () => {
const actual = pullOutImages(input);
assert.ok(verifyAst(actual));
assert.equal(astToString(actual), astToString(expected));
assert.deepStrictEqual(actual, expected);
});
@ -383,6 +385,7 @@ describe('Components.Markdown.transform', () => {
const actual = pullOutImages(input);
assert.ok(verifyAst(actual));
assert.equal(astToString(actual), astToString(expected));
assert.deepStrictEqual(actual, expected);
});
@ -434,7 +437,8 @@ describe('Components.Markdown.transform', () => {
const actual = pullOutImages(input);
assert.ok(verifyAst(actual));
assert.deepEqual(actual, expected);
assert.equal(astToString(actual), astToString(expected));
assert.deepStrictEqual(actual, expected);
});
it('paragraph with multiple images', () => {
@ -487,6 +491,7 @@ describe('Components.Markdown.transform', () => {
const actual = pullOutImages(input);
assert.ok(verifyAst(actual));
assert.equal(astToString(actual), astToString(expected));
assert.deepStrictEqual(actual, expected);
});
@ -643,6 +648,7 @@ describe('Components.Markdown.transform', () => {
const actual = pullOutImages(input);
assert.ok(verifyAst(actual));
assert.equal(astToString(actual), astToString(expected));
assert.deepStrictEqual(actual, expected);
});
@ -684,6 +690,7 @@ describe('Components.Markdown.transform', () => {
const actual = pullOutImages(input);
assert.ok(verifyAst(actual));
assert.equal(astToString(actual), astToString(expected));
assert.deepStrictEqual(actual, expected);
});
@ -745,6 +752,7 @@ describe('Components.Markdown.transform', () => {
const actual = pullOutImages(input);
assert.ok(verifyAst(actual));
assert.equal(astToString(actual), astToString(expected));
assert.deepStrictEqual(actual, expected);
});
@ -844,6 +852,7 @@ describe('Components.Markdown.transform', () => {
const actual = pullOutImages(input);
assert.ok(verifyAst(actual));
assert.equal(astToString(actual), astToString(expected));
assert.deepStrictEqual(actual, expected);
});
@ -917,6 +926,7 @@ describe('Components.Markdown.transform', () => {
const actual = pullOutImages(input);
assert.ok(verifyAst(actual));
assert.equal(astToString(actual), astToString(expected));
assert.deepStrictEqual(actual, expected);
});
@ -1131,6 +1141,7 @@ describe('Components.Markdown.transform', () => {
const actual = pullOutImages(input);
assert.ok(verifyAst(actual));
assert.equal(astToString(actual), astToString(expected));
assert.deepStrictEqual(actual, expected);
});
@ -1397,6 +1408,7 @@ describe('Components.Markdown.transform', () => {
const actual = pullOutImages(input);
assert.ok(verifyAst(actual));
assert.equal(astToString(actual), astToString(expected));
assert.deepStrictEqual(actual, expected);
});
@ -1699,7 +1711,8 @@ describe('Components.Markdown.transform', () => {
const actual = pullOutImages(input);
assert.ok(verifyAst(actual));
assert.deepEqual(actual, expected);
assert.equal(astToString(actual), astToString(expected));
assert.deepStrictEqual(actual, expected);
});
it('simple link', () => {
@ -1743,6 +1756,7 @@ describe('Components.Markdown.transform', () => {
const actual = pullOutImages(input);
assert.ok(verifyAst(actual));
assert.equal(astToString(actual), astToString(expected));
assert.deepStrictEqual(actual, expected);
});
@ -1808,6 +1822,157 @@ describe('Components.Markdown.transform', () => {
const actual = pullOutImages(input);
assert.ok(verifyAst(actual));
assert.equal(astToString(actual), astToString(expected));
assert.deepStrictEqual(actual, expected);
});
it('table', () => {
const input = makeAst({
type: 'document',
children: [{
type: 'table',
children: [{
type: 'table_row',
isHeading: true,
children: [{
type: 'table_cell',
isHeading: true,
children: [{
type: 'image',
destination: 'http://example.com/image',
children: [{
type: 'text',
literal: 'image1'
}]
}]
}, {
type: 'table_cell',
isHeading: true,
children: [{
type: 'text',
literal: 'This is '
}, {
type: 'image',
destination: 'http://example.com/image',
children: [{
type: 'text',
literal: 'image1'
}]
}, {
type: 'text',
literal: ' in a sentence.'
}]
}]
}, {
type: 'table_row',
isHeading: true,
children: [{
type: 'table_cell',
isHeading: true,
children: [{
type: 'text',
literal: 'This is '
}, {
type: 'image',
destination: 'http://example.com/image',
children: [{
type: 'text',
literal: 'image1'
}]
}, {
type: 'text',
literal: ' in a sentence.'
}]
}, {
type: 'table_cell',
isHeading: true,
children: [{
type: 'image',
destination: 'http://example.com/image',
children: [{
type: 'text',
literal: 'image1'
}]
}]
}]
}]
}]
});
const expected = makeAst({
type: 'document',
children: [{
type: 'table',
children: [{
type: 'table_row',
isHeading: true,
children: [{
type: 'table_cell',
isHeading: true,
children: [{
type: 'image',
destination: 'http://example.com/image',
children: [{
type: 'text',
literal: 'image1'
}]
}]
}, {
type: 'table_cell',
isHeading: true,
children: [{
type: 'text',
literal: 'This is '
}, {
type: 'image',
destination: 'http://example.com/image',
children: [{
type: 'text',
literal: 'image1'
}]
}, {
type: 'text',
literal: ' in a sentence.'
}]
}]
}, {
type: 'table_row',
isHeading: true,
children: [{
type: 'table_cell',
isHeading: true,
children: [{
type: 'text',
literal: 'This is '
}, {
type: 'image',
destination: 'http://example.com/image',
children: [{
type: 'text',
literal: 'image1'
}]
}, {
type: 'text',
literal: ' in a sentence.'
}]
}, {
type: 'table_cell',
isHeading: true,
children: [{
type: 'image',
destination: 'http://example.com/image',
children: [{
type: 'text',
literal: 'image1'
}]
}]
}]
}]
}]
});
const actual = pullOutImages(input);
assert.ok(verifyAst(actual));
assert.equal(astToString(actual), astToString(expected));
assert.deepStrictEqual(actual, expected);
});
});

View file

@ -1708,9 +1708,9 @@ commondir@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b"
commonmark-react-renderer@hmhealey/commonmark-react-renderer#1f078d53b7993d30b2762dd7d78fdd21ee84dd21:
commonmark-react-renderer@hmhealey/commonmark-react-renderer:
version "4.3.3"
resolved "https://codeload.github.com/hmhealey/commonmark-react-renderer/tar.gz/1f078d53b7993d30b2762dd7d78fdd21ee84dd21"
resolved "https://codeload.github.com/hmhealey/commonmark-react-renderer/tar.gz/86fa63f898802953842526c2030f3b63c5d1ae7a"
dependencies:
in-publish "^2.0.0"
lodash.assign "^4.2.0"
@ -1718,9 +1718,9 @@ commonmark-react-renderer@hmhealey/commonmark-react-renderer#1f078d53b7993d30b27
pascalcase "^0.1.1"
xss-filters "^1.2.6"
commonmark@hmhealey/commonmark.js#dda6d89198252d5fd4bb04c4cc0668766c22fd77:
commonmark@hmhealey/commonmark.js:
version "0.28.0"
resolved "https://codeload.github.com/hmhealey/commonmark.js/tar.gz/dda6d89198252d5fd4bb04c4cc0668766c22fd77"
resolved "https://codeload.github.com/hmhealey/commonmark.js/tar.gz/f7bc747151e6d856ceffa196044112e23d04e541"
dependencies:
entities "~ 1.1.1"
mdurl "~ 1.0.1"