summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorHarrison Healey <harrisonmhealey@gmail.com>2016-08-29 09:50:00 -0400
committerChristopher Speller <crspeller@gmail.com>2016-08-29 09:50:00 -0400
commit167dd22eefeeeb9c1eaebd990a4f5902bd366302 (patch)
tree6ddb15a80b2a608d42e20df72b98c0ae72821671
parent55342e8fe16613f06528ed1aa726231e9b597d26 (diff)
downloadchat-167dd22eefeeeb9c1eaebd990a4f5902bd366302.tar.gz
chat-167dd22eefeeeb9c1eaebd990a4f5902bd366302.tar.bz2
chat-167dd22eefeeeb9c1eaebd990a4f5902bd366302.zip
PLT-1752/PLT-3567/PLT-3998 Highlighting links in search, unit tests for autolinking (#3865)
* Added highlighting to links when their URL includes the search term * Decoupling UserStore from react-router to allow for unit tests involving it * PLT-3998 Added SiteURL as an option to be passed into the text formatting code * Removed reference to PreferenceStore and window from TextFormatting * Refactored TextFormatting to remove remaining browser-only code * Updated ChannelHeader and MessageWrapper to match the changes to TextFormatting * Increased max listeners for Preference and Emoji stores * PLT-3832 Added automated unit tests for autolinking * PLT-3567 Rerender posts when mention keywords change * Updated RHS and search to match the changes to TextFormatting * Broke TextFormatting's dependency on the UserStore
-rw-r--r--webapp/components/channel_header.jsx4
-rw-r--r--webapp/components/message_wrapper.jsx13
-rw-r--r--webapp/components/post_view/components/post.jsx7
-rw-r--r--webapp/components/post_view/components/post_body.jsx14
-rw-r--r--webapp/components/post_view/components/post_list.jsx2
-rw-r--r--webapp/components/post_view/components/post_message_container.jsx87
-rw-r--r--webapp/components/post_view/components/post_message_view.jsx66
-rw-r--r--webapp/components/post_view/post_view_controller.jsx16
-rw-r--r--webapp/components/rhs_comment.jsx10
-rw-r--r--webapp/components/rhs_root_post.jsx10
-rw-r--r--webapp/components/search_results_item.jsx16
-rw-r--r--webapp/root.jsx1
-rw-r--r--webapp/stores/emoji_store.jsx2
-rw-r--r--webapp/stores/preference_store.jsx10
-rw-r--r--webapp/stores/user_store.jsx2
-rw-r--r--webapp/tests/formatting_links.test.jsx504
-rw-r--r--webapp/utils/markdown.jsx30
-rw-r--r--webapp/utils/syntax_hightlighting.jsx10
-rw-r--r--webapp/utils/text_formatting.jsx79
-rw-r--r--webapp/utils/utils.jsx24
20 files changed, 765 insertions, 142 deletions
diff --git a/webapp/components/channel_header.jsx b/webapp/components/channel_header.jsx
index 65c856b4a..6cecc04bd 100644
--- a/webapp/components/channel_header.jsx
+++ b/webapp/components/channel_header.jsx
@@ -581,9 +581,9 @@ export default class ChannelHeader extends React.Component {
ref='headerOverlay'
>
<div
- onClick={TextFormatting.handleClick}
+ onClick={Utils.handleFormattedTextClick}
className='description'
- dangerouslySetInnerHTML={{__html: TextFormatting.formatText(channel.header, {singleline: true, mentionHighlight: false})}}
+ dangerouslySetInnerHTML={{__html: TextFormatting.formatText(channel.header, {singleline: true, mentionHighlight: false, siteURL: Utils.getSiteURL()})}}
/>
</OverlayTrigger>
</div>
diff --git a/webapp/components/message_wrapper.jsx b/webapp/components/message_wrapper.jsx
index 5e9939efa..4dba1024e 100644
--- a/webapp/components/message_wrapper.jsx
+++ b/webapp/components/message_wrapper.jsx
@@ -2,6 +2,7 @@
// See License.txt for license information.
import * as TextFormatting from 'utils/text_formatting.jsx';
+import * as Utils from 'utils/utils.jsx';
import React from 'react';
@@ -10,9 +11,19 @@ export default class MessageWrapper extends React.Component {
super(props);
this.state = {};
}
+
render() {
if (this.props.message) {
- return <div dangerouslySetInnerHTML={{__html: TextFormatting.formatText(this.props.message, this.props.options)}}/>;
+ const options = Object.assign({}, this.props.options, {
+ siteURL: Utils.getSiteURL()
+ });
+
+ return (
+ <div
+ onClick={Utils.handleFormattedTextClick}
+ dangerouslySetInnerHTML={{__html: TextFormatting.formatText(this.props.message, options)}}
+ />
+ );
}
return <div/>;
diff --git a/webapp/components/post_view/components/post.jsx b/webapp/components/post_view/components/post.jsx
index 1b94f717d..e9019bf38 100644
--- a/webapp/components/post_view/components/post.jsx
+++ b/webapp/components/post_view/components/post.jsx
@@ -114,10 +114,6 @@ export default class Post extends React.Component {
return true;
}
- if (nextProps.emojis !== this.props.emojis) {
- return true;
- }
-
if (nextState.dropdownOpened !== this.state.dropdownOpened) {
return true;
}
@@ -259,7 +255,6 @@ export default class Post extends React.Component {
handleCommentClick={this.handleCommentClick}
compactDisplay={this.props.compactDisplay}
previewCollapsed={this.props.previewCollapsed}
- emojis={this.props.emojis}
/>
</div>
</div>
@@ -279,7 +274,6 @@ Post.propTypes = {
isLastComment: React.PropTypes.bool,
shouldHighlight: React.PropTypes.bool,
displayNameType: React.PropTypes.string,
- hasProfiles: React.PropTypes.bool,
currentUser: React.PropTypes.object.isRequired,
center: React.PropTypes.bool,
compactDisplay: React.PropTypes.bool,
@@ -287,7 +281,6 @@ Post.propTypes = {
commentCount: React.PropTypes.number,
isCommentMention: React.PropTypes.bool,
useMilitaryTime: React.PropTypes.bool.isRequired,
- emojis: React.PropTypes.object.isRequired,
isFlagged: React.PropTypes.bool,
status: React.PropTypes.string
};
diff --git a/webapp/components/post_view/components/post_body.jsx b/webapp/components/post_view/components/post_body.jsx
index 348e7fc93..8c3b3009f 100644
--- a/webapp/components/post_view/components/post_body.jsx
+++ b/webapp/components/post_view/components/post_body.jsx
@@ -6,8 +6,8 @@ import UserStore from 'stores/user_store.jsx';
import * as Utils from 'utils/utils.jsx';
import * as GlobalActions from 'actions/global_actions.jsx';
import Constants from 'utils/constants.jsx';
-import * as TextFormatting from 'utils/text_formatting.jsx';
import PostBodyAdditionalContent from './post_body_additional_content.jsx';
+import PostMessageContainer from './post_message_container.jsx';
import PendingPostOptions from './pending_post_options.jsx';
import {FormattedMessage} from 'react-intl';
@@ -43,10 +43,6 @@ export default class PostBody extends React.Component {
return true;
}
- if (nextProps.emojis !== this.props.emojis) {
- return true;
- }
-
return false;
}
@@ -165,10 +161,7 @@ export default class PostBody extends React.Component {
);
} else {
message = (
- <span
- onClick={TextFormatting.handleClick}
- dangerouslySetInnerHTML={{__html: TextFormatting.formatText(this.props.post.message, {emojis: this.props.emojis})}}
- />
+ <PostMessageContainer post={this.props.post}/>
);
}
@@ -215,6 +208,5 @@ PostBody.propTypes = {
retryPost: React.PropTypes.func.isRequired,
handleCommentClick: React.PropTypes.func.isRequired,
compactDisplay: React.PropTypes.bool,
- previewCollapsed: React.PropTypes.string,
- emojis: React.PropTypes.object.isRequired
+ previewCollapsed: React.PropTypes.string
};
diff --git a/webapp/components/post_view/components/post_list.jsx b/webapp/components/post_view/components/post_list.jsx
index fc532c373..f891afdb3 100644
--- a/webapp/components/post_view/components/post_list.jsx
+++ b/webapp/components/post_view/components/post_list.jsx
@@ -314,7 +314,6 @@ export default class PostList extends React.Component {
compactDisplay={this.props.compactDisplay}
previewCollapsed={this.props.previewsCollapsed}
useMilitaryTime={this.props.useMilitaryTime}
- emojis={this.props.emojis}
isFlagged={isFlagged}
status={status}
/>
@@ -584,7 +583,6 @@ PostList.propTypes = {
previewsCollapsed: React.PropTypes.string,
useMilitaryTime: React.PropTypes.bool.isRequired,
isFocusPost: React.PropTypes.bool,
- emojis: React.PropTypes.object.isRequired,
flaggedPosts: React.PropTypes.object,
statuses: React.PropTypes.object
};
diff --git a/webapp/components/post_view/components/post_message_container.jsx b/webapp/components/post_view/components/post_message_container.jsx
new file mode 100644
index 000000000..4ab556fca
--- /dev/null
+++ b/webapp/components/post_view/components/post_message_container.jsx
@@ -0,0 +1,87 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React from 'react';
+
+import EmojiStore from 'stores/emoji_store.jsx';
+import PreferenceStore from 'stores/preference_store.jsx';
+import {Preferences} from 'utils/constants.jsx';
+import UserStore from 'stores/user_store.jsx';
+
+import PostMessageView from './post_message_view.jsx';
+
+export default class PostMessageContainer extends React.Component {
+ static propTypes = {
+ post: React.PropTypes.object.isRequired,
+ options: React.PropTypes.object
+ };
+
+ static defaultProps = {
+ options: {}
+ };
+
+ constructor(props) {
+ super(props);
+
+ this.onEmojiChange = this.onEmojiChange.bind(this);
+ this.onPreferenceChange = this.onPreferenceChange.bind(this);
+ this.onUserChange = this.onUserChange.bind(this);
+
+ const mentionKeys = UserStore.getCurrentMentionKeys();
+ mentionKeys.push('@here');
+
+ this.state = {
+ emojis: EmojiStore.getEmojis(),
+ enableFormatting: PreferenceStore.getBool(Preferences.CATEGORY_ADVANCED_SETTINGS, 'formatting', true),
+ mentionKeys,
+ usernameMap: UserStore.getProfilesUsernameMap()
+ };
+ }
+
+ componentDidMount() {
+ EmojiStore.addChangeListener(this.onEmojiChange);
+ PreferenceStore.addChangeListener(this.onPreferenceChange);
+ UserStore.addChangeListener(this.onUserChange);
+ }
+
+ componentWillUnmount() {
+ EmojiStore.removeChangeListener(this.onEmojiChange);
+ PreferenceStore.removeChangeListener(this.onPreferenceChange);
+ UserStore.removeChangeListener(this.onUserChange);
+ }
+
+ onEmojiChange() {
+ this.setState({
+ emojis: EmojiStore.getEmojis()
+ });
+ }
+
+ onPreferenceChange() {
+ this.setState({
+ enableFormatting: PreferenceStore.getBool(Preferences.CATEGORY_ADVANCED_SETTINGS, 'formatting', true)
+ });
+ }
+
+ onUserChange() {
+ const mentionKeys = UserStore.getCurrentMentionKeys();
+ mentionKeys.push('@here');
+
+ this.setState({
+ mentionKeys,
+ usernameMap: UserStore.getProfilesUsernameMap()
+ });
+ }
+
+ render() {
+ return (
+ <PostMessageView
+ options={this.props.options}
+ message={this.props.post.message}
+ emojis={this.state.emojis}
+ enableFormatting={this.state.enableFormatting}
+ mentionKeys={this.state.mentionKeys}
+ usernameMap={this.state.usernameMap}
+ />
+ );
+ }
+} \ No newline at end of file
diff --git a/webapp/components/post_view/components/post_message_view.jsx b/webapp/components/post_view/components/post_message_view.jsx
new file mode 100644
index 000000000..99589c973
--- /dev/null
+++ b/webapp/components/post_view/components/post_message_view.jsx
@@ -0,0 +1,66 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React from 'react';
+
+import * as TextFormatting from 'utils/text_formatting.jsx';
+import * as Utils from 'utils/utils.jsx';
+
+export default class PostMessageView extends React.Component {
+ static propTypes = {
+ options: React.PropTypes.object.isRequired,
+ message: React.PropTypes.string.isRequired,
+ emojis: React.PropTypes.object.isRequired,
+ enableFormatting: React.PropTypes.bool.isRequired,
+ mentionKeys: React.PropTypes.arrayOf(React.PropTypes.string).isRequired,
+ usernameMap: React.PropTypes.object.isRequired
+ };
+
+ shouldComponentUpdate(nextProps) {
+ if (!Utils.areObjectsEqual(nextProps.options, this.props.options)) {
+ return true;
+ }
+
+ if (nextProps.message !== this.props.message) {
+ return true;
+ }
+
+ // emojis are immutable
+ if (nextProps.emojis !== this.props.emojis) {
+ return true;
+ }
+
+ if (nextProps.enableFormatting !== this.props.enableFormatting) {
+ return true;
+ }
+
+ if (!Utils.areObjectsEqual(nextProps.mentionKeys, this.props.mentionKeys)) {
+ return true;
+ }
+
+ // Don't check if props.usernameMap changes since it is very large and inefficient to do so.
+ // This mimics previous behaviour, but could be changed if we decide it's worth it.
+
+ return false;
+ }
+
+ render() {
+ if (!this.props.enableFormatting) {
+ return <span>{this.props.message}</span>;
+ }
+
+ const options = Object.assign({}, this.props.options, {
+ emojis: this.props.emojis,
+ siteURL: Utils.getSiteURL(),
+ mentionKeys: this.props.mentionKeys,
+ usernameMap: this.props.usernameMap
+ });
+
+ return (
+ <span
+ onClick={Utils.handleFormattedTextClick}
+ dangerouslySetInnerHTML={{__html: TextFormatting.formatText(this.props.message, options)}}
+ />
+ );
+ }
+} \ No newline at end of file
diff --git a/webapp/components/post_view/post_view_controller.jsx b/webapp/components/post_view/post_view_controller.jsx
index 7e30818fb..58f8eff74 100644
--- a/webapp/components/post_view/post_view_controller.jsx
+++ b/webapp/components/post_view/post_view_controller.jsx
@@ -4,7 +4,6 @@
import PostList from './components/post_list.jsx';
import LoadingScreen from 'components/loading_screen.jsx';
-import EmojiStore from 'stores/emoji_store.jsx';
import PreferenceStore from 'stores/preference_store.jsx';
import UserStore from 'stores/user_store.jsx';
import PostStore from 'stores/post_store.jsx';
@@ -25,7 +24,6 @@ export default class PostViewController extends React.Component {
this.onPreferenceChange = this.onPreferenceChange.bind(this);
this.onUserChange = this.onUserChange.bind(this);
this.onPostsChange = this.onPostsChange.bind(this);
- this.onEmojisChange = this.onEmojisChange.bind(this);
this.onStatusChange = this.onStatusChange.bind(this);
this.onPostsViewJumpRequest = this.onPostsViewJumpRequest.bind(this);
this.onSetNewMessageIndicator = this.onSetNewMessageIndicator.bind(this);
@@ -67,7 +65,6 @@ export default class PostViewController extends React.Component {
compactDisplay: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.MESSAGE_DISPLAY, Preferences.MESSAGE_DISPLAY_DEFAULT) === Preferences.MESSAGE_DISPLAY_COMPACT,
previewsCollapsed: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.COLLAPSE_DISPLAY, 'false'),
useMilitaryTime: PreferenceStore.getBool(Constants.Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.USE_MILITARY_TIME, false),
- emojis: EmojiStore.getEmojis(),
flaggedPosts: PreferenceStore.getCategory(Constants.Preferences.CATEGORY_FLAGGED_POST)
};
}
@@ -123,12 +120,6 @@ export default class PostViewController extends React.Component {
});
}
- onEmojisChange() {
- this.setState({
- emojis: EmojiStore.getEmojis()
- });
- }
-
onStatusChange() {
const channel = this.state.channel;
let statuses;
@@ -145,7 +136,6 @@ export default class PostViewController extends React.Component {
UserStore.addStatusesChangeListener(this.onStatusChange);
PostStore.addChangeListener(this.onPostsChange);
PostStore.addPostsViewJumpListener(this.onPostsViewJumpRequest);
- EmojiStore.addChangeListener(this.onEmojisChange);
ChannelStore.addLastViewedListener(this.onSetNewMessageIndicator);
}
@@ -155,7 +145,6 @@ export default class PostViewController extends React.Component {
UserStore.removeStatusesChangeListener(this.onStatusChange);
PostStore.removeChangeListener(this.onPostsChange);
PostStore.removePostsViewJumpListener(this.onPostsViewJumpRequest);
- EmojiStore.removeChangeListener(this.onEmojisChange);
ChannelStore.removeLastViewedListener(this.onSetNewMessageIndicator);
}
@@ -298,10 +287,6 @@ export default class PostViewController extends React.Component {
return true;
}
- if (nextState.emojis !== this.state.emojis) {
- return true;
- }
-
return false;
}
@@ -332,7 +317,6 @@ export default class PostViewController extends React.Component {
useMilitaryTime={this.state.useMilitaryTime}
flaggedPosts={this.state.flaggedPosts}
lastViewed={this.state.lastViewed}
- emojis={this.state.emojis}
ownNewMessage={this.state.ownNewMessage}
statuses={this.state.statuses}
/>
diff --git a/webapp/components/rhs_comment.jsx b/webapp/components/rhs_comment.jsx
index 05df1ac5f..c9588eb33 100644
--- a/webapp/components/rhs_comment.jsx
+++ b/webapp/components/rhs_comment.jsx
@@ -4,6 +4,7 @@
import UserProfile from './user_profile.jsx';
import FileAttachmentList from './file_attachment_list.jsx';
import PendingPostOptions from 'components/post_view/components/pending_post_options.jsx';
+import PostMessageContainer from 'components/post_view/components/post_message_container.jsx';
import ProfilePicture from 'components/profile_picture.jsx';
import TeamStore from 'stores/team_store.jsx';
@@ -12,7 +13,6 @@ import UserStore from 'stores/user_store.jsx';
import * as GlobalActions from 'actions/global_actions.jsx';
import {flagPost, unflagPost} from 'actions/post_actions.jsx';
-import * as TextFormatting from 'utils/text_formatting.jsx';
import * as Utils from 'utils/utils.jsx';
import * as PostUtils from 'utils/post_utils.jsx';
@@ -234,13 +234,7 @@ export default class RhsComment extends React.Component {
}
let loading;
let postClass = '';
- let message = (
- <div
- ref='message_holder'
- onClick={TextFormatting.handleClick}
- dangerouslySetInnerHTML={{__html: TextFormatting.formatText(post.message)}}
- />
- );
+ let message = <PostMessageContainer post={post}/>;
if (post.state === Constants.POST_FAILED) {
postClass += ' post-fail';
diff --git a/webapp/components/rhs_root_post.jsx b/webapp/components/rhs_root_post.jsx
index cbb000922..ea0c71cc7 100644
--- a/webapp/components/rhs_root_post.jsx
+++ b/webapp/components/rhs_root_post.jsx
@@ -3,6 +3,7 @@
import UserProfile from './user_profile.jsx';
import PostBodyAdditionalContent from 'components/post_view/components/post_body_additional_content.jsx';
+import PostMessageContainer from 'components/post_view/components/post_message_container.jsx';
import FileAttachmentList from './file_attachment_list.jsx';
import ProfilePicture from 'components/profile_picture.jsx';
@@ -15,7 +16,6 @@ import {flagPost, unflagPost} from 'actions/post_actions.jsx';
import * as Utils from 'utils/utils.jsx';
import * as PostUtils from 'utils/post_utils.jsx';
-import * as TextFormatting from 'utils/text_formatting.jsx';
import Constants from 'utils/constants.jsx';
import {Tooltip, OverlayTrigger} from 'react-bootstrap';
@@ -305,13 +305,7 @@ export default class RhsRootPost extends React.Component {
profilePicContainer = '';
}
- const messageWrapper = (
- <div
- ref='message_holder'
- onClick={TextFormatting.handleClick}
- dangerouslySetInnerHTML={{__html: TextFormatting.formatText(post.message)}}
- />
- );
+ const messageWrapper = <PostMessageContainer post={post}/>;
let flag;
let flagFunc;
diff --git a/webapp/components/search_results_item.jsx b/webapp/components/search_results_item.jsx
index ada5e0ea6..2260f99ad 100644
--- a/webapp/components/search_results_item.jsx
+++ b/webapp/components/search_results_item.jsx
@@ -2,6 +2,7 @@
// See License.txt for license information.
import $ from 'jquery';
+import PostMessageContainer from 'components/post_view/components/post_message_container.jsx';
import UserProfile from './user_profile.jsx';
import TeamStore from 'stores/team_store.jsx';
@@ -11,7 +12,6 @@ import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
import * as GlobalActions from 'actions/global_actions.jsx';
import {flagPost, unflagPost} from 'actions/post_actions.jsx';
-import * as TextFormatting from 'utils/text_formatting.jsx';
import * as Utils from 'utils/utils.jsx';
import * as PostUtils from 'utils/post_utils.jsx';
@@ -78,11 +78,6 @@ export default class SearchResultsItem extends React.Component {
}
}
- const formattingOptions = {
- searchTerm: this.props.term,
- mentionHighlight: this.props.isMentionSearch
- };
-
let overrideUsername;
let disableProfilePopover = false;
if (post.props &&
@@ -251,9 +246,12 @@ export default class SearchResultsItem extends React.Component {
</li>
</ul>
<div className='search-item-snippet'>
- <span
- onClick={TextFormatting.handleClick}
- dangerouslySetInnerHTML={{__html: TextFormatting.formatText(post.message, formattingOptions)}}
+ <PostMessageContainer
+ post={post}
+ options={{
+ searchTerm: this.props.term,
+ mentionHighlight: this.props.isMentionSearch
+ }}
/>
</div>
</div>
diff --git a/webapp/root.jsx b/webapp/root.jsx
index d5b9a6d47..0fef23a53 100644
--- a/webapp/root.jsx
+++ b/webapp/root.jsx
@@ -16,6 +16,7 @@ import * as I18n from 'i18n/i18n.jsx';
import 'bootstrap-colorpicker/dist/css/bootstrap-colorpicker.css';
import 'google-fonts/google-fonts.css';
import 'sass/styles.scss';
+import 'katex/dist/katex.min.css';
// Import the root of our routing tree
import rRoot from 'routes/route_root.jsx';
diff --git a/webapp/stores/emoji_store.jsx b/webapp/stores/emoji_store.jsx
index e369885b4..076e671dd 100644
--- a/webapp/stores/emoji_store.jsx
+++ b/webapp/stores/emoji_store.jsx
@@ -17,6 +17,8 @@ class EmojiStore extends EventEmitter {
this.dispatchToken = AppDispatcher.register(this.handleEventPayload.bind(this));
+ this.setMaxListeners(600);
+
this.emojis = new Map(EmojiJson);
this.systemEmojis = new Map(EmojiJson);
diff --git a/webapp/stores/preference_store.jsx b/webapp/stores/preference_store.jsx
index 654036ae8..8aecfff40 100644
--- a/webapp/stores/preference_store.jsx
+++ b/webapp/stores/preference_store.jsx
@@ -8,7 +8,7 @@ import EventEmitter from 'events';
const CHANGE_EVENT = 'change';
-class PreferenceStoreClass extends EventEmitter {
+class PreferenceStore extends EventEmitter {
constructor() {
super();
@@ -17,7 +17,7 @@ class PreferenceStoreClass extends EventEmitter {
this.preferences = new Map();
- this.setMaxListeners(20);
+ this.setMaxListeners(600);
}
getKey(category, name) {
@@ -144,7 +144,5 @@ class PreferenceStoreClass extends EventEmitter {
}
}
-const PreferenceStore = new PreferenceStoreClass();
-PreferenceStore.setMaxListeners(25);
-export default PreferenceStore;
-global.window.PreferenceStore = PreferenceStore;
+global.PreferenceStore = new PreferenceStore();
+export default global.PreferenceStore; \ No newline at end of file
diff --git a/webapp/stores/user_store.jsx b/webapp/stores/user_store.jsx
index 6a47d0f65..859f385c0 100644
--- a/webapp/stores/user_store.jsx
+++ b/webapp/stores/user_store.jsx
@@ -334,7 +334,7 @@ class UserStoreClass extends EventEmitter {
}
var UserStore = new UserStoreClass();
-UserStore.setMaxListeners(16);
+UserStore.setMaxListeners(600);
UserStore.dispatchToken = AppDispatcher.register((payload) => {
var action = payload.action;
diff --git a/webapp/tests/formatting_links.test.jsx b/webapp/tests/formatting_links.test.jsx
new file mode 100644
index 000000000..237ef6121
--- /dev/null
+++ b/webapp/tests/formatting_links.test.jsx
@@ -0,0 +1,504 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import assert from 'assert';
+
+import * as Markdown from 'utils/markdown.jsx';
+import * as TextFormatting from 'utils/text_formatting.jsx';
+
+describe('Markdown.Links', function() {
+ this.timeout(10000);
+
+ it('Not links', function(done) {
+ assert.equal(
+ Markdown.format('example.com').trim(),
+ '<p>example.com</p>'
+ );
+
+ assert.equal(
+ Markdown.format('readme.md').trim(),
+ '<p>readme.md</p>'
+ );
+
+ assert.equal(
+ Markdown.format('@example.com').trim(),
+ '<p>@example.com</p>'
+ );
+
+ assert.equal(
+ Markdown.format('./make-compiled-client.sh').trim(),
+ '<p>./make-compiled-client.sh</p>'
+ );
+
+ assert.equal(
+ Markdown.format('test.:test').trim(),
+ '<p>test.:test</p>'
+ );
+
+ assert.equal(
+ Markdown.format('`https://example.com`').trim(),
+ '<p>' +
+ '<span class="codespan__pre-wrap">' +
+ '<code>' +
+ 'https://example.com' +
+ '</code>' +
+ '</span>' +
+ '</p>'
+ );
+
+ assert.equal(
+ Markdown.format('[link](example.com').trim(),
+ '<p>[link](example.com</p>'
+ );
+
+ done();
+ });
+
+ it('External links', function(done) {
+ assert.equal(
+ Markdown.format('http://example.com').trim(),
+ '<p><a class="theme markdown__link" href="http://example.com" rel="noreferrer" target="_blank">http://example.com</a></p>'
+ );
+
+ assert.equal(
+ Markdown.format('https://example.com').trim(),
+ '<p><a class="theme markdown__link" href="https://example.com" rel="noreferrer" target="_blank">https://example.com</a></p>'
+ );
+
+ assert.equal(
+ Markdown.format('www.example.com').trim(),
+ '<p><a class="theme markdown__link" href="http://www.example.com" rel="noreferrer" target="_blank">www.example.com</a></p>'
+ );
+
+ assert.equal(
+ Markdown.format('www.example.com/index').trim(),
+ '<p><a class="theme markdown__link" href="http://www.example.com/index" rel="noreferrer" target="_blank">www.example.com/index</a></p>'
+ );
+
+ assert.equal(
+ Markdown.format('www.example.com/index.html').trim(),
+ '<p><a class="theme markdown__link" href="http://www.example.com/index.html" rel="noreferrer" target="_blank">www.example.com/index.html</a></p>'
+ );
+
+ assert.equal(
+ Markdown.format('www.example.com/index/sub').trim(),
+ '<p><a class="theme markdown__link" href="http://www.example.com/index/sub" rel="noreferrer" target="_blank">www.example.com/index/sub</a></p>'
+ );
+
+ assert.equal(
+ Markdown.format('www1.example.com').trim(),
+ '<p><a class="theme markdown__link" href="http://www1.example.com" rel="noreferrer" target="_blank">www1.example.com</a></p>'
+ );
+
+ assert.equal(
+ Markdown.format('example.com/index').trim(),
+ '<p><a class="theme markdown__link" href="http://example.com/index" rel="noreferrer" target="_blank">example.com/index</a></p>'
+ );
+
+ done();
+ });
+
+ it('IP addresses', function(done) {
+ assert.equal(
+ Markdown.format('http://127.0.0.1').trim(),
+ '<p><a class="theme markdown__link" href="http://127.0.0.1" rel="noreferrer" target="_blank">http://127.0.0.1</a></p>'
+ );
+
+ assert.equal(
+ Markdown.format('http://192.168.1.1:4040').trim(),
+ '<p><a class="theme markdown__link" href="http://192.168.1.1:4040" rel="noreferrer" target="_blank">http://192.168.1.1:4040</a></p>'
+ );
+
+ assert.equal(
+ Markdown.format('http://[::1]:80').trim(),
+ '<p><a class="theme markdown__link" href="http://[::1]:80" rel="noreferrer" target="_blank">http://[::1]:80</a></p>'
+ );
+
+ assert.equal(
+ Markdown.format('http://[::1]:8065').trim(),
+ '<p><a class="theme markdown__link" href="http://[::1]:8065" rel="noreferrer" target="_blank">http://[::1]:8065</a></p>'
+ );
+
+ assert.equal(
+ Markdown.format('https://[::1]:80').trim(),
+ '<p><a class="theme markdown__link" href="https://[::1]:80" rel="noreferrer" target="_blank">https://[::1]:80</a></p>'
+ );
+
+ assert.equal(
+ Markdown.format('http://[2001:0:5ef5:79fb:303a:62d5:3312:ff42]:80').trim(),
+ '<p><a class="theme markdown__link" href="http://[2001:0:5ef5:79fb:303a:62d5:3312:ff42]:80" rel="noreferrer" target="_blank">http://[2001:0:5ef5:79fb:303a:62d5:3312:ff42]:80</a></p>'
+ );
+
+ assert.equal(
+ Markdown.format('http://[2001:0:5ef5:79fb:303a:62d5:3312:ff42]:8065').trim(),
+ '<p><a class="theme markdown__link" href="http://[2001:0:5ef5:79fb:303a:62d5:3312:ff42]:8065" rel="noreferrer" target="_blank">http://[2001:0:5ef5:79fb:303a:62d5:3312:ff42]:8065</a></p>'
+ );
+
+ assert.equal(
+ Markdown.format('https://[2001:0:5ef5:79fb:303a:62d5:3312:ff42]:443').trim(),
+ '<p><a class="theme markdown__link" href="https://[2001:0:5ef5:79fb:303a:62d5:3312:ff42]:443" rel="noreferrer" target="_blank">https://[2001:0:5ef5:79fb:303a:62d5:3312:ff42]:443</a></p>'
+ );
+
+ assert.equal(
+ Markdown.format('http://username:password@127.0.0.1').trim(),
+ '<p><a class="theme markdown__link" href="http://username:password@127.0.0.1" rel="noreferrer" target="_blank">http://username:password@127.0.0.1</a></p>'
+ );
+
+ assert.equal(
+ Markdown.format('http://username:password@[2001:0:5ef5:79fb:303a:62d5:3312:ff42]:80').trim(),
+ '<p><a class="theme markdown__link" href="http://username:password@[2001:0:5ef5:79fb:303a:62d5:3312:ff42]:80" rel="noreferrer" target="_blank">http://username:password@[2001:0:5ef5:79fb:303a:62d5:3312:ff42]:80</a></p>'
+ );
+
+ done();
+ });
+
+ it('Links with anchors', function(done) {
+ assert.equal(
+ Markdown.format('https://en.wikipedia.org/wiki/URLs#Syntax').trim(),
+ '<p><a class="theme markdown__link" href="https://en.wikipedia.org/wiki/URLs#Syntax" rel="noreferrer" target="_blank">https://en.wikipedia.org/wiki/URLs#Syntax</a></p>'
+ );
+
+ assert.equal(
+ Markdown.format('https://groups.google.com/forum/#!msg').trim(),
+ '<p><a class="theme markdown__link" href="https://groups.google.com/forum/#!msg" rel="noreferrer" target="_blank">https://groups.google.com/forum/#!msg</a></p>'
+ );
+
+ done();
+ });
+
+ it('Links with parameters', function(done) {
+ assert.equal(
+ Markdown.format('www.example.com/index?params=1').trim(),
+ '<p><a class="theme markdown__link" href="http://www.example.com/index?params=1" rel="noreferrer" target="_blank">www.example.com/index?params=1</a></p>'
+ );
+
+ assert.equal(
+ Markdown.format('www.example.com/index?params=1&other=2').trim(),
+ '<p><a class="theme markdown__link" href="http://www.example.com/index?params=1&amp;other=2" rel="noreferrer" target="_blank">www.example.com/index?params=1&amp;other=2</a></p>'
+ );
+
+ assert.equal(
+ Markdown.format('www.example.com/index?params=1;other=2').trim(),
+ '<p><a class="theme markdown__link" href="http://www.example.com/index?params=1;other=2" rel="noreferrer" target="_blank">www.example.com/index?params=1;other=2</a></p>'
+ );
+
+ assert.equal(
+ Markdown.format('http://example.com:8065').trim(),
+ '<p><a class="theme markdown__link" href="http://example.com:8065" rel="noreferrer" target="_blank">http://example.com:8065</a></p>'
+ );
+
+ assert.equal(
+ Markdown.format('http://username:password@example.com').trim(),
+ '<p><a class="theme markdown__link" href="http://username:password@example.com" rel="noreferrer" target="_blank">http://username:password@example.com</a></p>'
+ );
+
+ done();
+ });
+
+ it('Special characters', function(done) {
+ assert.equal(
+ Markdown.format('http://www.example.com/_/page').trim(),
+ '<p><a class="theme markdown__link" href="http://www.example.com/_/page" rel="noreferrer" target="_blank">http://www.example.com/_/page</a></p>'
+ );
+
+ assert.equal(
+ Markdown.format('www.example.com/_/page').trim(),
+ '<p><a class="theme markdown__link" href="http://www.example.com/_/page" rel="noreferrer" target="_blank">www.example.com/_/page</a></p>'
+ );
+
+ assert.equal(
+ Markdown.format('https://en.wikipedia.org/wiki/🐬').trim(),
+ '<p><a class="theme markdown__link" href="https://en.wikipedia.org/wiki/🐬" rel="noreferrer" target="_blank">https://en.wikipedia.org/wiki/🐬</a></p>'
+ );
+
+ assert.equal(
+ Markdown.format('http://✪df.ws/1234').trim(),
+ '<p><a class="theme markdown__link" href="http://✪df.ws/1234" rel="noreferrer" target="_blank">http://✪df.ws/1234</a></p>'
+ );
+
+ done();
+ });
+
+ it('Brackets', function(done) {
+ assert.equal(
+ Markdown.format('https://en.wikipedia.org/wiki/Rendering_(computer_graphics)').trim(),
+ '<p><a class="theme markdown__link" href="https://en.wikipedia.org/wiki/Rendering_(computer_graphics)" rel="noreferrer" target="_blank">https://en.wikipedia.org/wiki/Rendering_(computer_graphics)</a></p>'
+ );
+
+ assert.equal(
+ Markdown.format('http://example.com/more_(than)_one_(parens)').trim(),
+ '<p><a class="theme markdown__link" href="http://example.com/more_(than)_one_(parens)" rel="noreferrer" target="_blank">http://example.com/more_(than)_one_(parens)</a></p>'
+ );
+
+ assert.equal(
+ Markdown.format('http://example.com/(something)?after=parens').trim(),
+ '<p><a class="theme markdown__link" href="http://example.com/(something)?after=parens" rel="noreferrer" target="_blank">http://example.com/(something)?after=parens</a></p>'
+ );
+
+ assert.equal(
+ Markdown.format('http://foo.com/unicode_(✪)_in_parens').trim(),
+ '<p><a class="theme markdown__link" href="http://foo.com/unicode_(✪)_in_parens" rel="noreferrer" target="_blank">http://foo.com/unicode_(✪)_in_parens</a></p>'
+ );
+
+ done();
+ });
+
+ it('Email addresses', function(done) {
+ assert.equal(
+ Markdown.format('test@example.com').trim(),
+ '<p><a class="theme" href="mailto:test@example.com">test@example.com</a></p>'
+ );
+ assert.equal(
+ Markdown.format('test_underscore@example.com').trim(),
+ '<p><a class="theme" href="mailto:test_underscore@example.com">test_underscore@example.com</a></p>'
+ );
+
+ assert.equal(
+ Markdown.format('mailto:test@example.com').trim(),
+ '<p><a class="theme markdown__link" href="mailto:test@example.com" rel="noreferrer" target="_blank">mailto:test@example.com</a></p>'
+ );
+
+ done();
+ });
+
+ it('Formatted links', function(done) {
+ assert.equal(
+ Markdown.format('*https://example.com*').trim(),
+ '<p><em><a class="theme markdown__link" href="https://example.com" rel="noreferrer" target="_blank">https://example.com</a></em></p>'
+ );
+
+ assert.equal(
+ Markdown.format('_https://example.com_').trim(),
+ '<p><em><a class="theme markdown__link" href="https://example.com" rel="noreferrer" target="_blank">https://example.com</a></em></p>'
+ );
+
+ assert.equal(
+ Markdown.format('**https://example.com**').trim(),
+ '<p><strong><a class="theme markdown__link" href="https://example.com" rel="noreferrer" target="_blank">https://example.com</a></strong></p>'
+ );
+
+ assert.equal(
+ Markdown.format('__https://example.com__').trim(),
+ '<p><strong><a class="theme markdown__link" href="https://example.com" rel="noreferrer" target="_blank">https://example.com</a></strong></p>'
+ );
+
+ assert.equal(
+ Markdown.format('***https://example.com***').trim(),
+ '<p><strong><em><a class="theme markdown__link" href="https://example.com" rel="noreferrer" target="_blank">https://example.com</a></em></strong></p>'
+ );
+
+ assert.equal(
+ Markdown.format('___https://example.com___').trim(),
+ '<p><strong><em><a class="theme markdown__link" href="https://example.com" rel="noreferrer" target="_blank">https://example.com</a></em></strong></p>'
+ );
+
+ assert.equal(
+ Markdown.format('<https://example.com>').trim(),
+ '<p><a class="theme markdown__link" href="https://example.com" rel="noreferrer" target="_blank">https://example.com</a></p>'
+ );
+
+ assert.equal(
+ Markdown.format('<https://en.wikipedia.org/wiki/Rendering_(computer_graphics)>').trim(),
+ '<p><a class="theme markdown__link" href="https://en.wikipedia.org/wiki/Rendering_(computer_graphics)" rel="noreferrer" target="_blank">https://en.wikipedia.org/wiki/Rendering_(computer_graphics)</a></p>'
+ );
+
+ done();
+ });
+
+ it('Links with text', function(done) {
+ assert.equal(
+ Markdown.format('[example link](example.com)').trim(),
+ '<p><a class="theme markdown__link" href="http://example.com" rel="noreferrer" target="_blank">example link</a></p>'
+ );
+
+ assert.equal(
+ Markdown.format('[example.com](example.com)').trim(),
+ '<p><a class="theme markdown__link" href="http://example.com" rel="noreferrer" target="_blank">example.com</a></p>'
+ );
+
+ assert.equal(
+ Markdown.format('[example.com/other](example.com)').trim(),
+ '<p><a class="theme markdown__link" href="http://example.com" rel="noreferrer" target="_blank">example.com/other</a></p>'
+ );
+
+ assert.equal(
+ Markdown.format('[example.com/other_link](example.com/example)').trim(),
+ '<p><a class="theme markdown__link" href="http://example.com/example" rel="noreferrer" target="_blank">example.com/other_link</a></p>'
+ );
+
+ assert.equal(
+ Markdown.format('[link with spaces](example.com/ spaces in the url)').trim(),
+ '<p><a class="theme markdown__link" href="http://example.com/ spaces in the url" rel="noreferrer" target="_blank">link with spaces</a></p>'
+ );
+
+ assert.equal(
+ Markdown.format('[This whole #sentence should be a link](https://example.com)').trim(),
+ '<p><a class="theme markdown__link" href="https://example.com" rel="noreferrer" target="_blank">This whole #sentence should be a link</a></p>'
+ );
+
+ assert.equal(
+ Markdown.format('[email link](mailto:test@example.com)').trim(),
+ '<p><a class="theme markdown__link" href="mailto:test@example.com" rel="noreferrer" target="_blank">email link</a></p>'
+ );
+
+ assert.equal(
+ Markdown.format('[other link](ts3server://example.com)').trim(),
+ '<p><a class="theme markdown__link" href="ts3server://example.com" rel="noreferrer" target="_blank">other link</a></p>'
+ );
+
+ done();
+ });
+
+ it('Links with tooltips', function(done) {
+ assert.equal(
+ Markdown.format('[link](example.com "catch phrase!")').trim(),
+ '<p><a class="theme markdown__link" href="http://example.com" rel="noreferrer" target="_blank" title="catch phrase!">link</a></p>'
+ );
+
+ assert.equal(
+ Markdown.format('[link](example.com "title with "quotes"")').trim(),
+ '<p><a class="theme markdown__link" href="http://example.com" rel="noreferrer" target="_blank" title="title with &quot;quotes&quot;">link</a></p>'
+ );
+ assert.equal(
+ Markdown.format('[link with spaces](example.com/ spaces in the url "and a title")').trim(),
+ '<p><a class="theme markdown__link" href="http://example.com/ spaces in the url" rel="noreferrer" target="_blank" title="and a title">link with spaces</a></p>'
+ );
+
+ done();
+ });
+
+ it('Links with surrounding text', function(done) {
+ assert.equal(
+ Markdown.format('This is a sentence with a http://example.com in it.').trim(),
+ '<p>This is a sentence with a <a class="theme markdown__link" href="http://example.com" rel="noreferrer" target="_blank">http://example.com</a> in it.</p>'
+ );
+
+ assert.equal(
+ Markdown.format('This is a sentence with a http://example.com/_/underscore in it.').trim(),
+ '<p>This is a sentence with a <a class="theme markdown__link" href="http://example.com/_/underscore" rel="noreferrer" target="_blank">http://example.com/_/underscore</a> in it.</p>'
+ );
+
+ assert.equal(
+ Markdown.format('This is a sentence with a http://192.168.1.1:4040 in it.').trim(),
+ '<p>This is a sentence with a <a class="theme markdown__link" href="http://192.168.1.1:4040" rel="noreferrer" target="_blank">http://192.168.1.1:4040</a> in it.</p>'
+ );
+
+ assert.equal(
+ Markdown.format('This is a sentence with a https://[::1]:80 in it.').trim(),
+ '<p>This is a sentence with a <a class="theme markdown__link" href="https://[::1]:80" rel="noreferrer" target="_blank">https://[::1]:80</a> in it.</p>'
+ );
+
+ done();
+ });
+
+ it('Links with trailing punctuation', function(done) {
+ assert.equal(
+ Markdown.format('This is a link to http://example.com.').trim(),
+ '<p>This is a link to <a class="theme markdown__link" href="http://example.com" rel="noreferrer" target="_blank">http://example.com</a>.</p>'
+ );
+
+ assert.equal(
+ Markdown.format('This is a link containing http://example.com/something?with,commas,in,url, but not at the end').trim(),
+ '<p>This is a link containing <a class="theme markdown__link" href="http://example.com/something?with,commas,in,url" rel="noreferrer" target="_blank">http://example.com/something?with,commas,in,url</a>, but not at the end</p>'
+ );
+
+ assert.equal(
+ Markdown.format('This is a question about a link http://example.com?').trim(),
+ '<p>This is a question about a link <a class="theme markdown__link" href="http://example.com" rel="noreferrer" target="_blank">http://example.com</a>?</p>'
+ );
+
+ done();
+ });
+
+ it('Links with surrounding brackets', function(done) {
+ assert.equal(
+ Markdown.format('(http://example.com)').trim(),
+ '<p>(<a class="theme markdown__link" href="http://example.com" rel="noreferrer" target="_blank">http://example.com</a>)</p>'
+ );
+
+ assert.equal(
+ Markdown.format('(see http://example.com)').trim(),
+ '<p>(see <a class="theme markdown__link" href="http://example.com" rel="noreferrer" target="_blank">http://example.com</a>)</p>'
+ );
+
+ assert.equal(
+ Markdown.format('(http://example.com watch this)').trim(),
+ '<p>(<a class="theme markdown__link" href="http://example.com" rel="noreferrer" target="_blank">http://example.com</a> watch this)</p>'
+ );
+
+ assert.equal(
+ Markdown.format('(www.example.com)').trim(),
+ '<p>(<a class="theme markdown__link" href="http://www.example.com" rel="noreferrer" target="_blank">www.example.com</a>)</p>'
+ );
+
+ assert.equal(
+ Markdown.format('(see www.example.com)').trim(),
+ '<p>(see <a class="theme markdown__link" href="http://www.example.com" rel="noreferrer" target="_blank">www.example.com</a>)</p>'
+ );
+
+ assert.equal(
+ Markdown.format('(www.example.com watch this)').trim(),
+ '<p>(<a class="theme markdown__link" href="http://www.example.com" rel="noreferrer" target="_blank">www.example.com</a> watch this)</p>'
+ );
+ assert.equal(
+ Markdown.format('([link](http://example.com))').trim(),
+ '<p>(<a class="theme markdown__link" href="http://example.com" rel="noreferrer" target="_blank">link</a>)</p>'
+ );
+
+ assert.equal(
+ Markdown.format('(see [link](http://example.com))').trim(),
+ '<p>(see <a class="theme markdown__link" href="http://example.com" rel="noreferrer" target="_blank">link</a>)</p>'
+ );
+
+ assert.equal(
+ Markdown.format('([link](http://example.com) watch this)').trim(),
+ '<p>(<a class="theme markdown__link" href="http://example.com" rel="noreferrer" target="_blank">link</a> watch this)</p>'
+ );
+
+ assert.equal(
+ Markdown.format('(test@example.com)').trim(),
+ '<p>(<a class="theme" href="mailto:test@example.com">test@example.com</a>)</p>'
+ );
+
+ assert.equal(
+ Markdown.format('(email test@example.com)').trim(),
+ '<p>(email <a class="theme" href="mailto:test@example.com">test@example.com</a>)</p>'
+ );
+
+ assert.equal(
+ Markdown.format('(test@example.com email)').trim(),
+ '<p>(<a class="theme" href="mailto:test@example.com">test@example.com</a> email)</p>'
+ );
+
+ assert.equal(
+ Markdown.format('This is a sentence with a [link](http://example.com) in it.').trim(),
+ '<p>This is a sentence with a <a class="theme markdown__link" href="http://example.com" rel="noreferrer" target="_blank">link</a> in it.</p>'
+ );
+
+ assert.equal(
+ Markdown.format('This is a sentence with a link (http://example.com) in it.').trim(),
+ '<p>This is a sentence with a link (<a class="theme markdown__link" href="http://example.com" rel="noreferrer" target="_blank">http://example.com</a>) in it.</p>'
+ );
+
+ assert.equal(
+ Markdown.format('This is a sentence with a (https://en.wikipedia.org/wiki/Rendering_(computer_graphics)) in it.').trim(),
+ '<p>This is a sentence with a (<a class="theme markdown__link" href="https://en.wikipedia.org/wiki/Rendering_(computer_graphics)" rel="noreferrer" target="_blank">https://en.wikipedia.org/wiki/Rendering_(computer_graphics)</a>) in it.</p>'
+ );
+
+ done();
+ });
+
+ it('Searching for links', function(done) {
+ assert.equal(
+ TextFormatting.formatText('https://en.wikipedia.org/wiki/Unix', {searchTerm: 'wikipedia'}).trim(),
+ '<p><a class="theme markdown__link search-highlight" href="https://en.wikipedia.org/wiki/Unix" rel="noreferrer" target="_blank">https:/<wbr />/<wbr />en.wikipedia.org/<wbr />wiki/<wbr />Unix</a></p>'
+ );
+
+ assert.equal(
+ TextFormatting.formatText('[Link](https://en.wikipedia.org/wiki/Unix)', {searchTerm: 'unix'}).trim(),
+ '<p><a class="theme markdown__link search-highlight" href="https://en.wikipedia.org/wiki/Unix" rel="noreferrer" target="_blank">Link</a></p>'
+ );
+
+ done();
+ });
+});
diff --git a/webapp/utils/markdown.jsx b/webapp/utils/markdown.jsx
index 028e667fd..f6b218812 100644
--- a/webapp/utils/markdown.jsx
+++ b/webapp/utils/markdown.jsx
@@ -6,12 +6,11 @@ import * as SyntaxHighlighting from './syntax_hightlighting.jsx';
import marked from 'marked';
import katex from 'katex';
-import 'katex/dist/katex.min.css';
function markdownImageLoaded(image) {
image.style.height = 'auto';
}
-window.markdownImageLoaded = markdownImageLoaded;
+global.markdownImageLoaded = markdownImageLoaded;
class MattermostMarkdownRenderer extends marked.Renderer {
constructor(options, formattingOptions = {}) {
@@ -63,11 +62,11 @@ class MattermostMarkdownRenderer extends marked.Renderer {
const content = SyntaxHighlighting.highlight(usedLanguage, code);
let searchedContent = '';
- if (this.formattingOptions.searchTerm) {
+ if (this.formattingOptions.searchPatterns) {
const tokens = new Map();
let searched = TextFormatting.sanitizeHtml(code);
- searched = TextFormatting.highlightSearchTerms(searched, tokens, this.formattingOptions.searchTerm);
+ searched = TextFormatting.highlightSearchTerms(searched, tokens, this.formattingOptions.searchPatterns);
if (tokens.size > 0) {
searched = TextFormatting.replaceTokens(searched, tokens);
@@ -94,9 +93,9 @@ class MattermostMarkdownRenderer extends marked.Renderer {
codespan(text) {
let output = text;
- if (this.formattingOptions.searchTerm) {
+ if (this.formattingOptions.searchPatterns) {
const tokens = new Map();
- output = TextFormatting.highlightSearchTerms(output, tokens, this.formattingOptions.searchTerm);
+ output = TextFormatting.highlightSearchTerms(output, tokens, this.formattingOptions.searchPatterns);
output = TextFormatting.replaceTokens(output, tokens);
}
@@ -149,11 +148,22 @@ class MattermostMarkdownRenderer extends marked.Renderer {
outHref = `http://${outHref}`;
}
- let output = '<a class="theme markdown__link" href="' + outHref + '" rel="noreferrer"';
+ let output = '<a class="theme markdown__link';
+
+ if (this.formattingOptions.searchPatterns) {
+ for (const pattern of this.formattingOptions.searchPatterns) {
+ if (pattern.test(href)) {
+ output += ' search-highlight';
+ break;
+ }
+ }
+ }
+
+ output += '" href="' + outHref + '" rel="noreferrer"';
// special case for channel links and permalinks that are inside the app
- if (new RegExp('^' + TextFormatting.escapeRegex(global.mm_config.SiteURL) + '\\/[^\\/]+\\/(pl|channels)\\/').test(outHref)) {
- output += ' data-link="' + outHref.substring(global.mm_config.SiteURL.length) + '"';
+ if (this.formattingOptions.siteURL && new RegExp('^' + TextFormatting.escapeRegex(this.formattingOptions.siteURL) + '\\/[^\\/]+\\/(pl|channels)\\/').test(outHref)) {
+ output += ' data-link="' + outHref.substring(this.formattingOptions.siteURL) + '"';
} else {
output += ' target="_blank"';
}
@@ -201,7 +211,7 @@ class MattermostMarkdownRenderer extends marked.Renderer {
}
}
-export function format(text, options) {
+export function format(text, options = {}) {
const markdownOptions = {
renderer: new MattermostMarkdownRenderer(null, options),
sanitize: true,
diff --git a/webapp/utils/syntax_hightlighting.jsx b/webapp/utils/syntax_hightlighting.jsx
index ce904c41f..4146c43c5 100644
--- a/webapp/utils/syntax_hightlighting.jsx
+++ b/webapp/utils/syntax_hightlighting.jsx
@@ -1,7 +1,6 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-import * as Utils from './utils.jsx';
import * as TextFormatting from './text_formatting.jsx';
import Constants from './constants.jsx';
@@ -138,18 +137,17 @@ export function highlight(lang, code) {
}
export function getLanguageFromFilename(filename) {
- const fileInfo = Utils.splitFileLocation(filename);
- var ext = fileInfo.ext;
- if (!ext) {
- return null;
- }
+ const fileSplit = filename.split('.');
+ let ext = fileSplit.length > 1 ? fileSplit[fileSplit.length - 1] : '';
ext = ext.toLowerCase();
+
for (var key in HighlightedLanguages) {
if (HighlightedLanguages[key].extensions.find((x) => x === ext)) {
return key;
}
}
+
return null;
}
diff --git a/webapp/utils/text_formatting.jsx b/webapp/utils/text_formatting.jsx
index c110fc52f..f97c74625 100644
--- a/webapp/utils/text_formatting.jsx
+++ b/webapp/utils/text_formatting.jsx
@@ -2,15 +2,11 @@
// See License.txt for license information.
import Autolinker from 'autolinker';
-import {browserHistory} from 'react-router/es6';
import Constants from './constants.jsx';
import EmojiStore from 'stores/emoji_store.jsx';
import * as Emoticons from './emoticons.jsx';
import * as Markdown from './markdown.jsx';
-import PreferenceStore from 'stores/preference_store.jsx';
-import UserStore from 'stores/user_store.jsx';
import twemoji from 'twemoji';
-import * as Utils from './utils.jsx';
import XRegExp from 'xregexp';
// pattern to detect the existance of a Chinese, Japanese, or Korean character in a string
@@ -22,16 +18,19 @@ const cjkPattern = /[\u3000-\u303f\u3040-\u309f\u30a0-\u30ff\uff00-\uff9f\u4e00-
// as part of the second parameter:
// - searchTerm - If specified, this word is highlighted in the resulting html. Defaults to nothing.
// - mentionHighlight - Specifies whether or not to highlight mentions of the current user. Defaults to true.
+// - mentionKeys - A list of mention keys for the current user to highlight.
// - singleline - Specifies whether or not to remove newlines. Defaults to false.
// - emoticons - Enables emoticon parsing. Defaults to true.
// - markdown - Enables markdown parsing. Defaults to true.
-export function formatText(text, options = {}) {
+// - siteURL - The origin of this Mattermost instance. If provided, links to channels and posts will be replaced with internal
+// links that can be handled by a special click handler.
+// - usernameMap - An object mapping usernames to users. If provided, at mentions will be replaced with internal links that can
+// be handled by a special click handler (Utils.handleFormattedTextClick)
+export function formatText(text, inputOptions) {
let output = text;
- // would probably make more sense if it was on the calling components, but this option is intended primarily for debugging
- if (window.mm_config.EnableDeveloper === 'true' && PreferenceStore.get(Constants.Preferences.CATEGORY_ADVANCED_SETTINGS, 'formatting', 'true') === 'false') {
- return output;
- }
+ const options = Object.assign({}, inputOptions);
+ options.searchPatterns = parseSearchTerms(options.searchTerm).map(convertSearchTermToRegex);
if (!('markdown' in options) || options.markdown) {
// the markdown renderer will call doFormatText as necessary
@@ -58,7 +57,10 @@ export function doFormatText(text, options) {
const tokens = new Map();
// replace important words and phrases with tokens
- output = autolinkAtMentions(output, tokens);
+ if (options.usernameMap) {
+ output = autolinkAtMentions(output, tokens, options.usernameMap);
+ }
+
output = autolinkEmails(output, tokens);
output = autolinkHashtags(output, tokens);
@@ -66,12 +68,12 @@ export function doFormatText(text, options) {
output = Emoticons.handleEmoticons(output, tokens, options.emojis || EmojiStore.getEmojis());
}
- if (options.searchTerm) {
- output = highlightSearchTerms(output, tokens, options.searchTerm);
+ if (options.searchPatterns) {
+ output = highlightSearchTerms(output, tokens, options.searchPatterns);
}
if (!('mentionHighlight' in options) || options.mentionHighlight) {
- output = highlightCurrentMentions(output, tokens);
+ output = highlightCurrentMentions(output, tokens, options.mentionKeys);
}
if (!('emoticons' in options) || options.emoticon) {
@@ -143,10 +145,10 @@ function autolinkEmails(text, tokens) {
const punctuation = XRegExp.cache('[^\\pL\\d]');
-function autolinkAtMentions(text, tokens) {
+function autolinkAtMentions(text, tokens, usernameMap) {
// Test if provided text needs to be highlighted, special mention or current user
function mentionExists(u) {
- return (Constants.SPECIAL_MENTIONS.indexOf(u) !== -1 || UserStore.getProfileByUsername(u));
+ return (Constants.SPECIAL_MENTIONS.indexOf(u) !== -1 || !!usernameMap[u]);
}
function addToken(username, mention) {
@@ -200,12 +202,9 @@ export function escapeRegex(text) {
return text.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
}
-function highlightCurrentMentions(text, tokens) {
+function highlightCurrentMentions(text, tokens, mentionKeys = []) {
let output = text;
- const mentionKeys = UserStore.getCurrentMentionKeys();
- mentionKeys.push('@here');
-
// look for any existing tokens which are self mentions and should be highlighted
var newTokens = new Map();
for (const [alias, token] of tokens) {
@@ -239,7 +238,7 @@ function highlightCurrentMentions(text, tokens) {
return prefix + alias;
}
- for (const mention of UserStore.getCurrentMentionKeys()) {
+ for (const mention of mentionKeys) {
if (!mention) {
continue;
}
@@ -369,10 +368,8 @@ function convertSearchTermToRegex(term) {
return new RegExp(pattern, 'gi');
}
-export function highlightSearchTerms(text, tokens, searchTerm) {
- const terms = parseSearchTerms(searchTerm);
-
- if (terms.length === 0) {
+export function highlightSearchTerms(text, tokens, searchPatterns) {
+ if (!searchPatterns || searchPatterns.length === 0) {
return text;
}
@@ -390,13 +387,11 @@ export function highlightSearchTerms(text, tokens, searchTerm) {
return prefix + alias;
}
- for (const term of terms) {
+ for (const pattern of searchPatterns) {
// highlight existing tokens matching search terms
- const trimmedTerm = term.replace(/\*$/, '').toLowerCase();
var newTokens = new Map();
for (const [alias, token] of tokens) {
- if (token.originalText.toLowerCase() === trimmedTerm ||
- (token.hashtag && token.hashtag.toLowerCase() === trimmedTerm)) {
+ if (pattern.test(token.originalText)) {
const index = tokens.size + newTokens.size;
const newAlias = `MM_SEARCHTERM${index}`;
@@ -414,7 +409,7 @@ export function highlightSearchTerms(text, tokens, searchTerm) {
tokens.set(newToken[0], newToken[1]);
}
- output = output.replace(convertSearchTermToRegex(term), replaceSearchTermWithToken);
+ output = output.replace(pattern, replaceSearchTermWithToken);
}
return output;
@@ -438,32 +433,6 @@ function replaceNewlines(text) {
return text.replace(/\n/g, ' ');
}
-// A click handler that can be used with the results of TextFormatting.formatText to add default functionality
-// to clicked hashtags and @mentions.
-export function handleClick(e) {
- const mentionAttribute = e.target.getAttributeNode('data-mention');
- const hashtagAttribute = e.target.getAttributeNode('data-hashtag');
- const linkAttribute = e.target.getAttributeNode('data-link');
-
- if (mentionAttribute) {
- e.preventDefault();
-
- Utils.searchForTerm(mentionAttribute.value);
- } else if (hashtagAttribute) {
- e.preventDefault();
-
- Utils.searchForTerm(hashtagAttribute.value);
- } else if (linkAttribute) {
- const MIDDLE_MOUSE_BUTTON = 1;
-
- if (!(e.button === MIDDLE_MOUSE_BUTTON || e.altKey || e.ctrlKey || e.metaKey || e.shiftKey)) {
- e.preventDefault();
-
- browserHistory.push(linkAttribute.value);
- }
- }
-}
-
//replace all "/" inside <a> tags to "/<wbr />"
function insertLongLinkWbr(test) {
return test.replace(/\//g, (match, position, string) => {
diff --git a/webapp/utils/utils.jsx b/webapp/utils/utils.jsx
index a880c1783..3059ce529 100644
--- a/webapp/utils/utils.jsx
+++ b/webapp/utils/utils.jsx
@@ -1344,3 +1344,27 @@ export function isValidPassword(password) {
export function getSiteURL() {
return global.mm_config.SiteURL || window.location.origin;
}
+
+export function handleFormattedTextClick(e) {
+ const mentionAttribute = e.target.getAttributeNode('data-mention');
+ const hashtagAttribute = e.target.getAttributeNode('data-hashtag');
+ const linkAttribute = e.target.getAttributeNode('data-link');
+
+ if (mentionAttribute) {
+ e.preventDefault();
+
+ searchForTerm(mentionAttribute.value);
+ } else if (hashtagAttribute) {
+ e.preventDefault();
+
+ searchForTerm(hashtagAttribute.value);
+ } else if (linkAttribute) {
+ const MIDDLE_MOUSE_BUTTON = 1;
+
+ if (!(e.button === MIDDLE_MOUSE_BUTTON || e.altKey || e.ctrlKey || e.metaKey || e.shiftKey)) {
+ e.preventDefault();
+
+ browserHistory.push(linkAttribute.value);
+ }
+ }
+} \ No newline at end of file