mirror of
https://github.com/mattermost/mattermost.git
synced 2025-02-25 18:55:24 -06:00
Merge pull request #723 from hmhealey/plt17
PLT-17 Added Markdown support to the text formatter
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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",
|
||||
|
||||
16
web/react/utils/markdown.jsx
Normal file
16
web/react/utils/markdown.jsx
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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, ' ');
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user