MM-5957 Add mention highlighting on mobile (#2277)

* Switch pullOutImages to use a NodeWalker

* Add markdown transformer to combine adjacent text nodes

* MM-5957 Add mention highlighting on mobile
This commit is contained in:
Harrison Healey 2018-10-18 14:12:25 -04:00 committed by GitHub
parent 9e97fe160f
commit 78e5b99884
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 1052 additions and 31 deletions

View file

@ -5,12 +5,14 @@ import {connect} from 'react-redux';
import {getAutolinkedUrlSchemes} from 'mattermost-redux/selectors/entities/general';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {getCurrentUserMentionKeys} from 'mattermost-redux/selectors/entities/users';
import Markdown from './markdown';
function mapStateToProps(state) {
return {
autolinkedUrlSchemes: getAutolinkedUrlSchemes(state),
mentionKeys: getCurrentUserMentionKeys(state),
theme: getTheme(state),
};
}

View file

@ -30,7 +30,12 @@ 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';
import {
addListItemIndices,
combineTextNodes,
highlightMentions,
pullOutImages,
} from './transform';
export default class Markdown extends PureComponent {
static propTypes = {
@ -40,6 +45,7 @@ export default class Markdown extends PureComponent {
isEdited: PropTypes.bool,
isReplyPost: PropTypes.bool,
isSearchResult: PropTypes.bool,
mentionKeys: PropTypes.array.isRequired,
navigator: PropTypes.object.isRequired,
onChannelLinkPress: PropTypes.func,
onHashtagPress: PropTypes.func,
@ -111,6 +117,8 @@ export default class Markdown extends PureComponent {
table_row: MarkdownTableRow,
table_cell: MarkdownTableCell,
mention_highlight: Renderer.forwardChildren,
editedIndicator: this.renderEditedIndicator,
},
renderParagraphsInLists: true,
@ -141,9 +149,10 @@ export default class Markdown extends PureComponent {
// If this text is displayed, it will be styled by the image component
return <Text>{literal}</Text>;
}
const style = this.computeTextStyle(this.props.baseTextStyle, context);
// 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 style={style}>{literal}</Text>;
}
@ -390,8 +399,10 @@ export default class Markdown extends PureComponent {
render() {
let ast = this.parser.parse(this.props.value);
ast = combineTextNodes(ast);
ast = addListItemIndices(ast);
ast = pullOutImages(ast);
ast = highlightMentions(ast, this.props.mentionKeys);
if (this.props.isEdited) {
const editIndicatorNode = new Node('edited_indicator');

View file

@ -3,8 +3,45 @@
import {Node} from 'commonmark';
import {escapeRegex} from 'app/utils/markdown';
/* eslint-disable no-underscore-dangle */
// Combines adjacent text nodes into a single text node to make further transformation easier
export function combineTextNodes(ast) {
const walker = ast.walker();
let e;
while ((e = walker.next())) {
if (!e.entering) {
continue;
}
const node = e.node;
if (node.type !== 'text') {
continue;
}
while (node._next && node._next.type === 'text') {
const next = node._next;
node.literal += next.literal;
node._next = next._next;
if (node._next) {
node._next._prev = node;
}
if (node._parent._lastChild === next) {
node._parent._lastChild = node;
}
}
}
return ast;
}
// Add indices to the items of every list
export function addListItemIndices(ast) {
const walker = ast.walker();
@ -29,37 +66,29 @@ export function addListItemIndices(ast) {
return ast;
}
// Take all images, that aren't inside of tables, and move them to be children of the root document node.
// 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') {
const walker = ast.walker();
let e;
while ((e = walker.next())) {
if (!e.entering) {
continue;
}
let node = block.firstChild;
const node = e.node;
let cameFromChild = false;
// Skip tables because we'll render images inside of those as links
if (node.type === 'table') {
walker.resumeAt(node, false);
continue;
}
while (node && node !== block) {
if (node.type === 'image' && node.parent.type !== 'document') {
pullOutImage(node);
}
// Walk through tree to next node
if (node.firstChild && !cameFromChild) {
node = node.firstChild;
cameFromChild = false;
} else if (node.next) {
node = node.next;
cameFromChild = false;
} else {
node = node.parent;
cameFromChild = true;
}
if (node.type === 'image' && node.parent.type !== 'document') {
pullOutImage(node);
}
}
@ -219,3 +248,140 @@ function getLastSibling(node) {
return sibling;
}
export function highlightMentions(ast, mentionKeys) {
const walker = ast.walker();
// console.warn(mentionKeys);
let e;
while ((e = walker.next())) {
if (!e.entering) {
continue;
}
const node = e.node;
if (node.type === 'text') {
const {index, mention} = getFirstMention(node.literal, mentionKeys);
if (index === -1 || !mention) {
continue;
}
const mentionNode = highlightTextNode(node, index, index + mention.key.length, 'mention_highlight');
// Resume processing on the next node after the mention node which may include any remaining text
// that was part of this one
walker.resumeAt(mentionNode, false);
} else if (node.type === 'at_mention') {
const matches = mentionKeys.some((mention) => {
const mentionName = '@' + node.mentionName;
if (mention.caseSensitive) {
return mention.key === mentionName;
}
return mention.key.toLowerCase() === mentionName.toLowerCase();
});
if (!matches) {
continue;
}
const wrapper = new Node('mention_highlight');
wrapNode(wrapper, node);
// Skip processing the wrapper to prevent checking this node again as its child
walker.resumeAt(wrapper, false);
}
}
return ast;
}
// Given a string and an array of mention keys, returns the first mention that appears and its index.
export function getFirstMention(str, mentionKeys) {
let firstMention = null;
let firstMentionIndex = -1;
for (const mention of mentionKeys) {
const flags = mention.caseSensitive ? '' : 'i';
const pattern = new RegExp(`\\b${escapeRegex(mention.key)}_*\\b`, flags);
const match = pattern.exec(str);
if (!match) {
continue;
}
if (firstMentionIndex === -1 || match.index < firstMentionIndex) {
firstMentionIndex = match.index;
firstMention = mention;
}
}
return {
index: firstMentionIndex,
mention: firstMention,
};
}
// Given a text node, start/end indices, and a highlight node type, splits it into up to three nodes:
// the text before the highlight (if any exists), the highlighted text, and the text after the highlight
// the end of the highlight (if any exists). Returns a node containing the highlighted text.
export function highlightTextNode(node, start, end, type) {
const literal = node.literal;
node.literal = literal.substring(start, end);
// Start by wrapping the node and then re-insert any non-highlighted code around it
const highlighted = new Node(type);
wrapNode(highlighted, node);
if (start !== 0) {
const before = new Node('text');
before.literal = literal.substring(0, start);
highlighted.insertBefore(before);
}
if (end !== literal.length) {
const after = new Node('text');
after.literal = literal.substring(end);
highlighted.insertAfter(after);
}
return highlighted;
}
// Wraps a given node in another node of the given type. The wrapper will take the place of
// the node in the AST relative to its parents and siblings, and it will have the node as
// its only child.
function wrapNode(wrapper, node) {
// Set parent and update parent's children if necessary
wrapper._parent = node._parent;
if (node._parent._firstChild === node) {
node._parent._firstChild = wrapper;
}
if (node._parent._lastChild === node) {
node._parent._lastChild = wrapper;
}
// Set siblings and update those if necessary
wrapper._prev = node._prev;
node._prev = null;
if (wrapper._prev) {
wrapper._prev._next = wrapper;
}
wrapper._next = node._next;
node._next = null;
if (wrapper._next) {
wrapper._next._prev = wrapper;
}
// Make node a child of wrapper
wrapper._firstChild = node;
wrapper._lastChild = node;
node._parent = wrapper;
}

View file

@ -6,14 +6,186 @@ import {Node, Parser} from 'commonmark';
import {
addListItemIndices,
combineTextNodes,
getFirstMention,
highlightMentions,
highlightTextNode,
pullOutImages,
} from 'app/components/markdown/transform';
/* eslint-disable max-nested-callbacks, no-console */
/* eslint-disable no-console, no-underscore-dangle */
describe('Components.Markdown.transform', () => {
const parser = new Parser();
describe('combineTextNodes', () => {
const tests = [{
name: 'no text nodes',
input: {
type: 'document',
children: [{
type: 'thematic_break',
}],
},
expected: {
type: 'document',
children: [{
type: 'thematic_break',
}],
},
}, {
name: 'one text node',
input: {
type: 'document',
children: [{
type: 'paragraph',
children: [{
type: 'text',
literal: 'This is a sentence',
}],
}],
},
expected: {
type: 'document',
children: [{
type: 'paragraph',
children: [{
type: 'text',
literal: 'This is a sentence',
}],
}],
},
}, {
name: 'multiple text nodes',
input: {
type: 'document',
children: [{
type: 'paragraph',
children: [{
type: 'text',
literal: 'This ',
}, {
type: 'text',
literal: 'is a',
}, {
type: 'text',
literal: ' sen',
}, {
type: 'text',
literal: 'tence',
}],
}],
},
expected: {
type: 'document',
children: [{
type: 'paragraph',
children: [{
type: 'text',
literal: 'This is a sentence',
}],
}],
},
}, {
name: 'mixed formatting',
input: {
type: 'document',
children: [{
type: 'paragraph',
children: [{
type: 'text',
literal: 'This ',
}, {
type: 'emph',
children: [{
type: 'text',
literal: 'is a',
}],
}, {
type: 'text',
literal: ' sen',
}, {
type: 'text',
literal: 'tence',
}],
}],
},
expected: {
type: 'document',
children: [{
type: 'paragraph',
children: [{
type: 'text',
literal: 'This ',
}, {
type: 'emph',
children: [{
type: 'text',
literal: 'is a',
}],
}, {
type: 'text',
literal: ' sentence',
}],
}],
},
}, {
name: 'multiple paragraphs',
input: {
type: 'document',
children: [{
type: 'paragraph',
children: [{
type: 'text',
literal: 'This is a ',
}, {
type: 'text',
literal: 'paragraph',
}],
}, {
type: 'paragraph',
children: [{
type: 'text',
literal: 'This is',
}, {
type: 'text',
literal: ' another ',
}, {
type: 'text',
literal: 'paragraph',
}],
}],
},
expected: {
type: 'document',
children: [{
type: 'paragraph',
children: [{
type: 'text',
literal: 'This is a paragraph',
}],
}, {
type: 'paragraph',
children: [{
type: 'text',
literal: 'This is another paragraph',
}],
}],
},
}];
for (const test of tests) {
it(test.name, () => {
const input = makeAst(test.input);
const expected = makeAst(test.expected);
const actual = combineTextNodes(input);
assert.ok(verifyAst(actual));
assert.deepStrictEqual(actual, expected);
});
}
});
describe('addListItemIndices', () => {
it('unordered list', () => {
const input = makeAst({
@ -2056,31 +2228,678 @@ describe('Components.Markdown.transform', () => {
assert.deepStrictEqual(actual, expected);
});
});
describe('highlightMentions', () => {
const tests = [{
name: 'no mentions',
input: 'These are words',
mentionKeys: [],
expected: {
type: 'document',
children: [{
type: 'paragraph',
children: [{
type: 'text',
literal: 'These are words',
}],
}],
},
}, {
name: 'not an at-mention',
input: 'These are words',
mentionKeys: [{key: 'words'}],
expected: {
type: 'document',
children: [{
type: 'paragraph',
children: [{
type: 'text',
literal: 'These are ',
}, {
type: 'mention_highlight',
children: [{
type: 'text',
literal: 'words',
}],
}],
}],
},
}, {
name: 'at-mention for another user',
input: 'This is @user',
mentionKeys: [{key: '@words'}],
expected: {
type: 'document',
children: [{
type: 'paragraph',
children: [{
type: 'text',
literal: 'This is ',
}, {
type: 'at_mention',
_mentionName: 'user',
children: [{
type: 'text',
literal: '@user',
}],
}],
}],
},
}, {
name: 'at-mention',
input: 'These are @words',
mentionKeys: [{key: '@words'}],
expected: {
type: 'document',
children: [{
type: 'paragraph',
children: [{
type: 'text',
literal: 'These are ',
}, {
type: 'mention_highlight',
children: [{
type: 'at_mention',
_mentionName: 'words',
children: [{
type: 'text',
literal: '@words',
}],
}],
}],
}],
},
}, {
name: 'at-mention and non-at-mention for same word',
input: 'These are @words',
mentionKeys: [{key: 'words'}, {key: '@words'}],
expected: {
type: 'document',
children: [{
type: 'paragraph',
children: [{
type: 'text',
literal: 'These are ',
}, {
type: 'mention_highlight',
children: [{
type: 'at_mention',
_mentionName: 'words',
children: [{
type: 'text',
literal: '@words',
}],
}],
}],
}],
},
}, {
name: 'case insensitive mentions',
input: 'These are Words and wORDS',
mentionKeys: [{key: 'words'}],
expected: {
type: 'document',
children: [{
type: 'paragraph',
children: [{
type: 'text',
literal: 'These are ',
}, {
type: 'mention_highlight',
children: [{
type: 'text',
literal: 'Words',
}],
}, {
type: 'text',
literal: ' and ',
}, {
type: 'mention_highlight',
children: [{
type: 'text',
literal: 'wORDS',
}],
}],
}],
},
}, {
name: 'case sesitive mentions',
input: 'These are Words and wORDS',
mentionKeys: [{key: 'Words', caseSensitive: true}],
expected: {
type: 'document',
children: [{
type: 'paragraph',
children: [{
type: 'text',
literal: 'These are ',
}, {
type: 'mention_highlight',
children: [{
type: 'text',
literal: 'Words',
}],
}, {
type: 'text',
literal: ' and wORDS',
}],
}],
},
}, {
name: 'bold',
input: 'These are **words** in a sentence',
mentionKeys: [{key: 'words'}],
expected: {
type: 'document',
children: [{
type: 'paragraph',
children: [{
type: 'text',
literal: 'These are ',
}, {
type: 'strong',
children: [{
type: 'mention_highlight',
children: [{
type: 'text',
literal: 'words',
}],
}],
}, {
type: 'text',
literal: ' in a sentence',
}],
}],
},
}, {
name: 'italics',
input: 'These _are Words in_ a sentence',
mentionKeys: [{key: 'words'}],
expected: {
type: 'document',
children: [{
type: 'paragraph',
children: [{
type: 'text',
literal: 'These ',
}, {
type: 'emph',
children: [{
type: 'text',
literal: 'are ',
}, {
type: 'mention_highlight',
children: [{
type: 'text',
literal: 'Words',
}],
}, {
type: 'text',
literal: ' in',
}],
}, {
type: 'text',
literal: ' a sentence',
}],
}],
},
}, {
name: 'code span',
input: 'These are `words`',
mentionKeys: [{key: 'words'}],
expected: {
type: 'document',
children: [{
type: 'paragraph',
children: [{
type: 'text',
literal: 'These are ',
}, {
type: 'code',
literal: 'words',
}],
}],
},
}, {
name: 'code block',
input: '```\nThese are\nwords\n```',
mentionKeys: [{key: 'words'}],
expected: {
type: 'document',
children: [{
type: 'code_block',
literal: 'These are\nwords\n',
}],
},
}, {
name: 'link text',
input: 'These are [words words](https://example.com)',
mentionKeys: [{key: 'words'}],
expected: {
type: 'document',
children: [{
type: 'paragraph',
children: [{
type: 'text',
literal: 'These are ',
}, {
type: 'link',
destination: 'https://example.com',
title: '',
children: [{
type: 'mention_highlight',
children: [{
type: 'text',
literal: 'words',
}],
}, {
type: 'text',
literal: ' ',
}, {
type: 'mention_highlight',
children: [{
type: 'text',
literal: 'words',
}],
}],
}],
}],
},
}, {
name: 'link url',
input: 'This is [a link](https://example.com/words)',
mentionKeys: [{key: 'example'}, {key: 'com'}, {key: 'https'}, {key: 'words'}],
expected: {
type: 'document',
children: [{
type: 'paragraph',
children: [{
type: 'text',
literal: 'This is ',
}, {
type: 'link',
destination: 'https://example.com/words',
title: '',
children: [{
type: 'text',
literal: 'a link',
}],
}],
}],
},
}, {
name: 'autolinked url',
input: 'https://example.com/words',
mentionKeys: [{key: 'example'}, {key: 'com'}, {key: 'https'}, {key: 'words'}],
expected: {
type: 'document',
children: [{
type: 'paragraph',
children: [{
type: 'link',
destination: 'https://example.com/words',
title: '',
children: [{
type: 'mention_highlight',
children: [{
type: 'text',
literal: 'https',
}],
}, {
type: 'text',
literal: '://',
}, {
type: 'mention_highlight',
children: [{
type: 'text',
literal: 'example',
}],
}, {
type: 'text',
literal: '.',
}, {
type: 'mention_highlight',
children: [{
type: 'text',
literal: 'com',
}],
}, {
type: 'text',
literal: '/',
}, {
type: 'mention_highlight',
children: [{
type: 'text',
literal: 'words',
}],
}],
}],
}],
},
}, {
name: 'words with punctuation',
input: 'words. (words) words/words/words words:words',
mentionKeys: [{key: 'words'}],
expected: {
type: 'document',
children: [{
type: 'paragraph',
children: [{
type: 'mention_highlight',
children: [{
type: 'text',
literal: 'words',
}],
}, {
type: 'text',
literal: '. (',
}, {
type: 'mention_highlight',
children: [{
type: 'text',
literal: 'words',
}],
}, {
type: 'text',
literal: ') ',
}, {
type: 'mention_highlight',
children: [{
type: 'text',
literal: 'words',
}],
}, {
type: 'text',
literal: '/',
}, {
type: 'mention_highlight',
children: [{
type: 'text',
literal: 'words',
}],
}, {
type: 'text',
literal: '/',
}, {
type: 'mention_highlight',
children: [{
type: 'text',
literal: 'words',
}],
}, {
type: 'text',
literal: ' ',
}, {
type: 'link',
destination: 'words:words',
title: '',
children: [{
type: 'mention_highlight',
children: [{
type: 'text',
literal: 'words',
}],
}, {
type: 'text',
literal: ':',
}, {
type: 'mention_highlight',
children: [{
type: 'text',
literal: 'words',
}],
}],
}],
}],
},
}];
for (const test of tests) {
it(test.name, () => {
const input = combineTextNodes(parser.parse(test.input));
const expected = makeAst(test.expected);
const actual = highlightMentions(input, test.mentionKeys);
assert.ok(verifyAst(actual));
assert.deepStrictEqual(stripUnusedFields(actual), stripUnusedFields(expected));
});
}
});
describe('getFirstMention', () => {
const tests = [{
name: 'no mention keys',
input: 'apple banana orange',
mentionKeys: [],
expected: {index: -1, mention: null},
}, {
name: 'single mention',
input: 'apple banana orange',
mentionKeys: [{key: 'banana'}],
expected: {index: 6, mention: {key: 'banana'}},
}, {
name: 'multiple mentions',
input: 'apple banana orange',
mentionKeys: [{key: 'apple'}, {key: 'orange'}],
expected: {index: 0, mention: {key: 'apple'}},
}, {
name: 'case sensitive',
input: 'apple APPLE Apple aPPle',
mentionKeys: [{key: 'Apple', caseSensitive: true}],
expected: {index: 12, mention: {key: 'Apple', caseSensitive: true}},
}, {
name: 'followed by period',
input: 'banana.',
mentionKeys: [{key: 'banana'}],
expected: {index: 0, mention: {key: 'banana'}},
}, {
name: 'followed by underscores',
input: 'banana__',
mentionKeys: [{key: 'banana'}],
expected: {index: 0, mention: {key: 'banana'}},
}, {
name: 'in brackets',
input: '(banana)',
mentionKeys: [{key: 'banana'}],
expected: {index: 1, mention: {key: 'banana'}},
}, {
name: 'following punctuation',
input: ':banana',
mentionKeys: [{key: 'banana'}],
expected: {index: 1, mention: {key: 'banana'}},
}, {
name: 'not part of another word',
input: 'pineapple',
mentionKeys: [{key: 'apple'}],
expected: {index: -1, mention: null},
}, {
name: 'no error from weird mention keys',
input: 'apple banana orange',
mentionKeys: [{key: '*\\3_.'}],
expected: {index: -1, mention: null},
}];
for (const test of tests) {
it(test.name, () => {
const actual = getFirstMention(test.input, test.mentionKeys);
assert.deepStrictEqual(actual, test.expected);
});
}
});
describe('highlightTextNode', () => {
const type = 'my_highlight';
const literal = 'This is a sentence';
const tests = [{
name: 'highlight entire text',
start: 0,
end: literal.length,
expected: [{
type,
children: [{
type: 'text',
literal,
}],
}],
}, {
name: 'highlight start of text',
start: 0,
end: 6,
expected: [{
type,
children: [{
type: 'text',
literal: 'This i',
}],
}, {
type: 'text',
literal: 's a sentence',
}],
}, {
name: 'highlight end of text',
start: 8,
end: literal.length,
expected: [{
type: 'text',
literal: 'This is ',
}, {
type,
children: [{
type: 'text',
literal: 'a sentence',
}],
}],
}, {
name: 'highlight middle of text',
start: 5,
end: 12,
expected: [{
type: 'text',
literal: 'This ',
}, {
type,
children: [{
type: 'text',
literal: 'is a se',
}],
}, {
type: 'text',
literal: 'ntence',
}],
}];
for (const test of tests) {
it(test.name + ', without siblings', () => {
const actual = makeAst({
type: 'parent',
children: [{
type: 'text',
literal,
}],
});
const expected = makeAst({
type: 'parent',
children: test.expected,
});
const node = actual.firstChild;
assert.equal(node.type, 'text');
const highlighted = highlightTextNode(node, test.start, test.end, type);
assert.equal(highlighted.type, type);
assert.ok(verifyAst(actual));
assert.deepStrictEqual(actual, expected);
});
it(test.name + ', with siblings', () => {
const actual = makeAst({
type: 'parent',
children: [{
type: 'previous',
}, {
type: 'text',
literal,
}, {
type: 'next',
}],
});
const expected = makeAst({
type: 'parent',
children: [
{
type: 'previous',
},
...test.expected,
{
type: 'next',
},
],
});
const node = actual.firstChild.next;
assert.equal(node.type, 'text');
const highlighted = highlightTextNode(node, test.start, test.end, type);
assert.equal(highlighted.type, type);
assert.ok(verifyAst(actual));
assert.deepStrictEqual(actual, expected);
});
}
});
});
// Testing and debugging functions
// Confirms that all parent, child, and sibling linkages are correct and go both ways.
function verifyAst(node) {
if (node.prev && node.prev.next !== node) {
console.error('node is not linked properly to prev');
return false;
}
if (node.next && node.next.prev !== node) {
console.error('node is not linked properly to prev');
return false;
}
if (!node.firstChild && node.lastChild) {
console.error('node has children, but is not linked to first child');
return false;
}
if (node.firstChild && !node.lastChild) {
console.error('node has children, but is not linked to last child');
return false;
}
for (let child = node.firstChild; child; child = child.next) {
if (child.parent !== node) {
console.error('node is not linked properly to child');
return false;
}
if (!verifyAst(child)) {
return false;
}
if (!child.next && child !== node.lastChild) {
console.error('node children are not linked correctly');
return false;
}
}
if (node.firstChild && node.firstChild.prev) {
console.error('node\'s first child has previous sibling');
return false;
}
if (node.lastChild && node.lastChild.next) {
console.error('node\'s last child has next sibling');
return false;
}
return true;
@ -2206,3 +3025,23 @@ function makeAst(input) {
return node;
}
// Remove any fields from the AST that are only used while parsing to make testing equality easier.
function stripUnusedFields(node) {
const walker = node.walker();
let e;
while ((e = walker.next())) {
e.node._open = false;
e.node._size = null;
e.node._sourcepos = null;
e.node._fenceChar = null;
e.node._fenceLength = 0;
e.node._fenceOffset = 0;
e.node._info = '';
e.node._isFenced = false;
}
return node;
}

View file

@ -89,6 +89,9 @@ export const getMarkdownTextStyles = makeStyleSheetFromTheme((theme) => {
table_header_row: {
fontWeight: '700',
},
mention_highlight: {
backgroundColor: theme.mentionHighlightBg,
},
};
});

8
package-lock.json generated
View file

@ -5176,8 +5176,8 @@
"integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs="
},
"commonmark": {
"version": "github:mattermost/commonmark.js#95999d98559338f0249ffb3ab72984b48eccea42",
"from": "github:mattermost/commonmark.js#95999d9",
"version": "github:mattermost/commonmark.js#d5a7a7bfb373778d3bfd4575962c10fb3f3909c6",
"from": "github:mattermost/commonmark.js#d5a7a7bfb373778d3bfd4575962c10fb3f3909c6",
"requires": {
"entities": "~ 1.1.1",
"mdurl": "~ 1.0.1",
@ -5187,8 +5187,8 @@
}
},
"commonmark-react-renderer": {
"version": "github:mattermost/commonmark-react-renderer#b560513b93357ee5fd33489fe311bc65d4658870",
"from": "github:mattermost/commonmark-react-renderer#b560513b93357ee5fd33489fe311bc65d4658870",
"version": "github:mattermost/commonmark-react-renderer#ea502501b1f2c31c6b3ff6d40eead2c808f40d42",
"from": "github:mattermost/commonmark-react-renderer#ea502501b1f2c31c6b3ff6d40eead2c808f40d42",
"requires": {
"in-publish": "^2.0.0",
"lodash.assign": "^4.2.0",

View file

@ -10,8 +10,8 @@
"@babel/polyfill": "7.0.0",
"@babel/runtime": "7.0.0",
"analytics-react-native": "1.2.0",
"commonmark": "github:mattermost/commonmark.js#95999d9",
"commonmark-react-renderer": "github:mattermost/commonmark-react-renderer#b560513b93357ee5fd33489fe311bc65d4658870",
"commonmark": "github:mattermost/commonmark.js#d5a7a7bfb373778d3bfd4575962c10fb3f3909c6",
"commonmark-react-renderer": "github:mattermost/commonmark-react-renderer#ea502501b1f2c31c6b3ff6d40eead2c808f40d42",
"deep-equal": "1.0.1",
"fuse.js": "3.2.1",
"intl": "1.2.5",