summaryrefslogtreecommitdiffstats
path: root/web/react
diff options
context:
space:
mode:
Diffstat (limited to 'web/react')
-rw-r--r--web/react/components/new_channel_modal.jsx10
-rw-r--r--web/react/components/post_body.jsx4
-rw-r--r--web/react/package.json3
-rw-r--r--web/react/utils/markdown.jsx16
-rw-r--r--web/react/utils/text_formatting.jsx82
5 files changed, 89 insertions, 26 deletions
diff --git a/web/react/components/new_channel_modal.jsx b/web/react/components/new_channel_modal.jsx
index fc7b8c183..1488a1431 100644
--- a/web/react/components/new_channel_modal.jsx
+++ b/web/react/components/new_channel_modal.jsx
@@ -107,8 +107,8 @@ export default class NewChannelModal extends React.Component {
{channelSwitchText}
</div>
<div className={displayNameClass}>
- <label className='col-sm-2 form__label control-label'>{'Name'}</label>
- <div className='col-sm-10'>
+ <label className='col-sm-3 form__label control-label'>{'Name'}</label>
+ <div className='col-sm-9'>
<input
onChange={this.handleChange}
type='text'
@@ -121,7 +121,7 @@ export default class NewChannelModal extends React.Component {
tabIndex='1'
/>
{displayNameError}
- <p className='input__help'>
+ <p className='input__help dark'>
{'Channel URL: ' + prettyTeamURL + this.props.channelData.name + ' ('}
<a
href='#'
@@ -134,11 +134,11 @@ export default class NewChannelModal extends React.Component {
</div>
</div>
<div className='form-group less'>
- <div className='col-sm-2'>
+ <div className='col-sm-3'>
<label className='form__label control-label'>{'Description'}</label>
<label className='form__label light'>{'(optional)'}</label>
</div>
- <div className='col-sm-10'>
+ <div className='col-sm-9'>
<textarea
className='form-control no-resize'
ref='channel_desc'
diff --git a/web/react/components/post_body.jsx b/web/react/components/post_body.jsx
index 3be615bb9..8020714cd 100644
--- a/web/react/components/post_body.jsx
+++ b/web/react/components/post_body.jsx
@@ -154,7 +154,7 @@ export default class PostBody extends React.Component {
return (
<div className='post-body'>
{comment}
- <p
+ <div
key={`${post.id}_message`}
id={`${post.id}_message`}
className={postClass}
@@ -164,7 +164,7 @@ export default class PostBody extends React.Component {
onClick={TextFormatting.handleClick}
dangerouslySetInnerHTML={{__html: TextFormatting.formatText(this.state.message)}}
/>
- </p>
+ </div>
{fileAttachmentHolder}
{embed}
</div>
diff --git a/web/react/package.json b/web/react/package.json
index 04e0f6bab..dd7d45f8a 100644
--- a/web/react/package.json
+++ b/web/react/package.json
@@ -9,7 +9,8 @@
"object-assign": "3.0.0",
"react-zeroclipboard-mixin": "0.1.0",
"twemoji": "1.4.1",
- "babel-runtime": "5.8.24"
+ "babel-runtime": "5.8.24",
+ "marked": "0.3.5"
},
"devDependencies": {
"browserify": "11.0.1",
diff --git a/web/react/utils/markdown.jsx b/web/react/utils/markdown.jsx
new file mode 100644
index 000000000..8c63810cd
--- /dev/null
+++ b/web/react/utils/markdown.jsx
@@ -0,0 +1,16 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+const marked = require('marked');
+
+export class MattermostMarkdownRenderer extends marked.Renderer {
+ link(href, title, text) {
+ let outHref = href;
+
+ if (outHref.lastIndexOf('http', 0) !== 0) {
+ outHref = `http://${outHref}`;
+ }
+
+ return super.link(outHref, title, text);
+ }
+}
diff --git a/web/react/utils/text_formatting.jsx b/web/react/utils/text_formatting.jsx
index 54d010dbf..4e390f708 100644
--- a/web/react/utils/text_formatting.jsx
+++ b/web/react/utils/text_formatting.jsx
@@ -3,21 +3,38 @@
const Autolinker = require('autolinker');
const Constants = require('./constants.jsx');
+const Markdown = require('./markdown.jsx');
const UserStore = require('../stores/user_store.jsx');
const Utils = require('./utils.jsx');
+const marked = require('marked');
+
+const markdownRenderer = new Markdown.MattermostMarkdownRenderer();
+
// Performs formatting of user posts including highlighting mentions and search terms and converting urls, hashtags, and
// @mentions to links by taking a user's message and returning a string of formatted html. Also takes a number of options
// 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.
// - singleline - Specifies whether or not to remove newlines. Defaults to false.
+// - markdown - Enables markdown parsing. Defaults to true.
export function formatText(text, options = {}) {
- let output = sanitizeHtml(text);
+ if (!('markdown' in options)) {
+ options.markdown = true;
+ }
+
+ // wait until marked can sanitize the html so that we don't break markdown block quotes
+ let output;
+ if (!options.markdown) {
+ output = sanitizeHtml(text);
+ } else {
+ output = text;
+ }
+
const tokens = new Map();
// replace important words and phrases with tokens
- output = autolinkUrls(output, tokens);
+ output = autolinkUrls(output, tokens, !!options.markdown);
output = autolinkAtMentions(output, tokens);
output = autolinkHashtags(output, tokens);
@@ -29,11 +46,21 @@ export function formatText(text, options = {}) {
output = highlightCurrentMentions(output, tokens);
}
+ // perform markdown parsing while we have an html-free input string
+ if (options.markdown) {
+ output = marked(output, {
+ renderer: markdownRenderer,
+ sanitize: true
+ });
+ }
+
// reinsert tokens with formatted versions of the important words and phrases
output = replaceTokens(output, tokens);
// replace newlines with html line breaks
- output = replaceNewlines(output, options.singleline);
+ if (options.singleline) {
+ output = replaceNewlines(output);
+ }
return output;
}
@@ -51,7 +78,7 @@ export function sanitizeHtml(text) {
return output;
}
-function autolinkUrls(text, tokens) {
+function autolinkUrls(text, tokens, markdown) {
function replaceUrlWithToken(autolinker, match) {
const linkText = match.getMatchedText();
let url = linkText;
@@ -61,7 +88,7 @@ function autolinkUrls(text, tokens) {
}
const index = tokens.size;
- const alias = `__MM_LINK${index}__`;
+ const alias = `MM_LINK${index}`;
tokens.set(alias, {
value: `<a class='theme' target='_blank' href='${url}'>${linkText}</a>`,
@@ -81,7 +108,30 @@ function autolinkUrls(text, tokens) {
replaceFn: replaceUrlWithToken
});
- return autolinker.link(text);
+ let output = text;
+
+ // temporarily replace markdown links if markdown is enabled so that we don't accidentally parse them twice
+ const markdownLinkTokens = new Map();
+ if (markdown) {
+ function replaceMarkdownLinkWithToken(markdownLink) {
+ const index = markdownLinkTokens.size;
+ const alias = `MM_MARKDOWNLINK${index}`;
+
+ markdownLinkTokens.set(alias, {value: markdownLink});
+
+ return alias;
+ }
+
+ output = output.replace(/\]\([^\)]*\)/g, replaceMarkdownLinkWithToken);
+ }
+
+ output = autolinker.link(output);
+
+ if (markdown) {
+ output = replaceTokens(output, markdownLinkTokens);
+ }
+
+ return output;
}
function autolinkAtMentions(text, tokens) {
@@ -91,7 +141,7 @@ function autolinkAtMentions(text, tokens) {
const usernameLower = username.toLowerCase();
if (Constants.SPECIAL_MENTIONS.indexOf(usernameLower) !== -1 || UserStore.getProfileByUsername(usernameLower)) {
const index = tokens.size;
- const alias = `__MM_ATMENTION${index}__`;
+ const alias = `MM_ATMENTION${index}`;
tokens.set(alias, {
value: `<a class='mention-link' href='#' data-mention='${usernameLower}'>${mention}</a>`,
@@ -119,7 +169,7 @@ function highlightCurrentMentions(text, tokens) {
for (const [alias, token] of tokens) {
if (mentionKeys.indexOf(token.originalText) !== -1) {
const index = tokens.size + newTokens.size;
- const newAlias = `__MM_SELFMENTION${index}__`;
+ const newAlias = `MM_SELFMENTION${index}`;
newTokens.set(newAlias, {
value: `<span class='mention-highlight'>${alias}</span>`,
@@ -138,7 +188,7 @@ function highlightCurrentMentions(text, tokens) {
// look for self mentions in the text
function replaceCurrentMentionWithToken(fullMatch, prefix, mention) {
const index = tokens.size;
- const alias = `__MM_SELFMENTION${index}__`;
+ const alias = `MM_SELFMENTION${index}`;
tokens.set(alias, {
value: `<span class='mention-highlight'>${mention}</span>`,
@@ -162,7 +212,7 @@ function autolinkHashtags(text, tokens) {
for (const [alias, token] of tokens) {
if (token.originalText.lastIndexOf('#', 0) === 0) {
const index = tokens.size + newTokens.size;
- const newAlias = `__MM_HASHTAG${index}__`;
+ const newAlias = `MM_HASHTAG${index}`;
newTokens.set(newAlias, {
value: `<a class='mention-link' href='#' data-hashtag='${token.originalText}'>${token.originalText}</a>`,
@@ -181,7 +231,7 @@ function autolinkHashtags(text, tokens) {
// look for hashtags in the text
function replaceHashtagWithToken(fullMatch, prefix, hashtag) {
const index = tokens.size;
- const alias = `__MM_HASHTAG${index}__`;
+ const alias = `MM_HASHTAG${index}`;
tokens.set(alias, {
value: `<a class='mention-link' href='#' data-hashtag='${hashtag}'>${hashtag}</a>`,
@@ -201,7 +251,7 @@ function highlightSearchTerm(text, tokens, searchTerm) {
for (const [alias, token] of tokens) {
if (token.originalText === searchTerm) {
const index = tokens.size + newTokens.size;
- const newAlias = `__MM_SEARCHTERM${index}__`;
+ const newAlias = `MM_SEARCHTERM${index}`;
newTokens.set(newAlias, {
value: `<span class='search-highlight'>${alias}</span>`,
@@ -219,7 +269,7 @@ function highlightSearchTerm(text, tokens, searchTerm) {
function replaceSearchTermWithToken(fullMatch, prefix, word) {
const index = tokens.size;
- const alias = `__MM_SEARCHTERM${index}__`;
+ const alias = `MM_SEARCHTERM${index}`;
tokens.set(alias, {
value: `<span class='search-highlight'>${word}</span>`,
@@ -246,11 +296,7 @@ function replaceTokens(text, tokens) {
return output;
}
-function replaceNewlines(text, singleline) {
- if (!singleline) {
- return text.replace(/\n/g, '<br />');
- }
-
+function replaceNewlines(text) {
return text.replace(/\n/g, ' ');
}