diff options
Diffstat (limited to 'web/react/utils')
-rw-r--r-- | web/react/utils/client.jsx | 25 | ||||
-rw-r--r-- | web/react/utils/markdown.jsx | 82 | ||||
-rw-r--r-- | web/react/utils/text_formatting.jsx | 32 | ||||
-rw-r--r-- | web/react/utils/utils.jsx | 70 |
4 files changed, 142 insertions, 67 deletions
diff --git a/web/react/utils/client.jsx b/web/react/utils/client.jsx index bf117b3b3..aeb39d8a8 100644 --- a/web/react/utils/client.jsx +++ b/web/react/utils/client.jsx @@ -589,21 +589,38 @@ export function updateChannel(channel, success, error) { track('api', 'api_channels_update'); } -export function updateChannelDesc(data, success, error) { +export function updateChannelHeader(data, success, error) { $.ajax({ - url: '/api/v1/channels/update_desc', + url: '/api/v1/channels/update_header', dataType: 'json', contentType: 'application/json', type: 'POST', data: JSON.stringify(data), success, error: function onError(xhr, status, err) { - var e = handleError('updateChannelDesc', xhr, status, err); + var e = handleError('updateChannelHeader', xhr, status, err); error(e); } }); - track('api', 'api_channels_desc'); + track('api', 'api_channels_header'); +} + +export function updateChannelPurpose(data, success, error) { + $.ajax({ + url: '/api/v1/channels/update_purpose', + dataType: 'json', + contentType: 'application/json', + type: 'POST', + data: JSON.stringify(data), + success, + error: function onError(xhr, status, err) { + var e = handleError('updateChannelPurpose', xhr, status, err); + error(e); + } + }); + + track('api', 'api_channels_purpose'); } export function updateNotifyProps(data, success, error) { diff --git a/web/react/utils/markdown.jsx b/web/react/utils/markdown.jsx index 26587dd6e..e34f3d00a 100644 --- a/web/react/utils/markdown.jsx +++ b/web/react/utils/markdown.jsx @@ -6,7 +6,43 @@ const Utils = require('./utils.jsx'); const marked = require('marked'); -export class MattermostMarkdownRenderer extends marked.Renderer { +class MattermostInlineLexer extends marked.InlineLexer { + constructor(links, options) { + super(links, options); + + this.rules = Object.assign({}, this.rules); + + // modified version of the regex that doesn't break up words in snake_case, + // allows for links starting with www, and allows links succounded by parentheses + // the original is /^[\s\S]+?(?=[\\<!\[_*`~]|https?:\/\/| {2,}\n|$)/ + this.rules.text = /^[\s\S]+?(?=[^\w\/]_|[\\<!\[*`~]|https?:\/\/|www\.|\(| {2,}\n|$)/; + + // modified version of the regex that allows links starting with www and those surrounded + // by parentheses + // the original is /^(https?:\/\/[^\s<]+[^<.,:;"')\]\s])/ + this.rules.url = /^(\(?(?:https?:\/\/|www\.)[^\s<.][^\s<]*[^<.,:;"'\]\s])/; + + // modified version of the regex that allows <links> starting with www. + // the original is /^<([^ >]+(@|:\/)[^ >]+)>/ + this.rules.autolink = /^<((?:[^ >]+(@|:\/)|www\.)[^ >]+)>/; + } +} + +class MattermostParser extends marked.Parser { + parse(src) { + this.inline = new MattermostInlineLexer(src.links, this.options, this.renderer); + this.tokens = src.reverse(); + + var out = ''; + while (this.next()) { + out += this.tok(); + } + + return out; + } +} + +class MattermostMarkdownRenderer extends marked.Renderer { constructor(options, formattingOptions = {}) { super(options); @@ -32,8 +68,20 @@ export class MattermostMarkdownRenderer extends marked.Renderer { link(href, title, text) { let outHref = href; + let outText = text; + let prefix = ''; + let suffix = ''; + + // some links like https://en.wikipedia.org/wiki/Rendering_(computer_graphics) contain brackets + // and we try our best to differentiate those from ones just wrapped in brackets when autolinking + if (outHref.startsWith('(') && outHref.endsWith(')') && text === outHref) { + prefix = '('; + suffix = ')'; + outText = text.substring(1, text.length - 1); + outHref = outHref.substring(1, outHref.length - 1); + } - if (!(/^(mailto|https?|ftp)/.test(outHref))) { + if (!(/[a-z+.-]+:/i).test(outHref)) { outHref = `http://${outHref}`; } @@ -48,26 +96,17 @@ export class MattermostMarkdownRenderer extends marked.Renderer { output += ' target="_blank">'; } - output += text + '</a>'; + output += outText + '</a>'; - return output; + return prefix + output + suffix; } paragraph(text) { - let outText = text; - - // required so markdown does not strip '_' from @user_names - outText = TextFormatting.doFormatMentions(text); - - if (!('emoticons' in this.options) || this.options.emoticon) { - outText = TextFormatting.doFormatEmoticons(outText); - } - if (this.formattingOptions.singleline) { - return `<p class="markdown__paragraph-inline">${outText}</p>`; + return `<p class="markdown__paragraph-inline">${text}</p>`; } - return super.paragraph(outText); + return super.paragraph(text); } table(header, body) { @@ -78,3 +117,16 @@ export class MattermostMarkdownRenderer extends marked.Renderer { return TextFormatting.doFormatText(txt, this.formattingOptions); } } + +export function format(text, options) { + const markdownOptions = { + renderer: new MattermostMarkdownRenderer(null, options), + sanitize: true, + gfm: true + }; + + const tokens = marked.lexer(text, markdownOptions); + + return new MattermostParser(markdownOptions).parse(tokens); +} + diff --git a/web/react/utils/text_formatting.jsx b/web/react/utils/text_formatting.jsx index 9f1a5a53f..2de858a17 100644 --- a/web/react/utils/text_formatting.jsx +++ b/web/react/utils/text_formatting.jsx @@ -8,8 +8,6 @@ const Markdown = require('./markdown.jsx'); const UserStore = require('../stores/user_store.jsx'); const Utils = require('./utils.jsx'); -const marked = require('marked'); - // 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: @@ -22,11 +20,8 @@ export function formatText(text, options = {}) { let output; if (!('markdown' in options) || options.markdown) { - // the markdown renderer will call doFormatText as necessary so just call marked - output = marked(text, { - renderer: new Markdown.MattermostMarkdownRenderer(null, options), - sanitize: true - }); + // the markdown renderer will call doFormatText as necessary + output = Markdown.format(text, options); } else { output = sanitizeHtml(text); output = doFormatText(output, options); @@ -48,7 +43,7 @@ export function doFormatText(text, options) { // replace important words and phrases with tokens output = autolinkAtMentions(output, tokens); - output = autolinkUrls(output, tokens); + output = autolinkEmails(output, tokens); output = autolinkHashtags(output, tokens); if (!('emoticons' in options) || options.emoticon) { @@ -98,28 +93,21 @@ export function sanitizeHtml(text) { return output; } -// Convert URLs into tokens -function autolinkUrls(text, tokens) { - function replaceUrlWithToken(autolinker, match) { +// Convert emails into tokens +function autolinkEmails(text, tokens) { + function replaceEmailWithToken(autolinker, match) { const linkText = match.getMatchedText(); let url = linkText; if (match.getType() === 'email') { url = `mailto:${url}`; - } else if (!(/^(mailto|https?|ftp)/.test(url))) { - url = `http://${url}`; } const index = tokens.size; - const alias = `MM_LINK${index}`; - - var target = 'target="_blank"'; - if (url.lastIndexOf(Utils.getTeamURLFromAddressBar(), 0) === 0) { - target = ''; - } + const alias = `MM_EMAIL${index}`; tokens.set(alias, { - value: `<a class="theme" ${target} href="${url}">${linkText}</a>`, + value: `<a class="theme" href="${url}">${linkText}</a>`, originalText: linkText }); @@ -128,12 +116,12 @@ function autolinkUrls(text, tokens) { // we can't just use a static autolinker because we need to set replaceFn const autolinker = new Autolinker({ - urls: true, + urls: false, email: true, phone: false, twitter: false, hashtag: false, - replaceFn: replaceUrlWithToken + replaceFn: replaceEmailWithToken }); return autolinker.link(text); diff --git a/web/react/utils/utils.jsx b/web/react/utils/utils.jsx index b643c6012..35ce49ae2 100644 --- a/web/react/utils/utils.jsx +++ b/web/react/utils/utils.jsx @@ -231,46 +231,62 @@ export function getTimestamp() { return Date.now(); } -function testUrlMatch(text) { - var urlMatcher = new Autolinker.matchParser.MatchParser({ +// extracts links not styled by Markdown +export function extractLinks(text) { + const urlMatcher = new Autolinker.matchParser.MatchParser({ urls: true, emails: false, twitter: false, phone: false, hashtag: false }); - var result = []; + const links = []; + let replaceText = text; + + // pull out the Markdown code blocks + const codeBlocks = []; + const splitText = replaceText.split('`'); // also handles ``` + for (let i = 1; i < splitText.length; i += 2) { + if (splitText[i].trim() !== '') { + codeBlocks.push(splitText[i]); + } + } + function replaceFn(match) { - var linkData = {}; - var matchText = match.getMatchedText(); + let link = ''; + const matchText = match.getMatchedText(); + const tempText = replaceText; + + const start = replaceText.indexOf(matchText); + const end = start + matchText.length; + + replaceText = replaceText.substring(0, start) + replaceText.substring(end); + + // if it's a Markdown link, just skip it + if (start > 1) { + if (tempText.charAt(start - 2) === ']' && tempText.charAt(start - 1) === '(' && tempText.charAt(end) === ')') { + return; + } + } + + // if it's in a Markdown code block, skip it + for (const i in codeBlocks) { + if (codeBlocks[i].indexOf(matchText) === 0) { + codeBlocks[i] = codeBlocks[i].replace(matchText, ''); + return; + } + } - linkData.text = matchText; if (matchText.trim().indexOf('http') === 0) { - linkData.link = matchText; + link = matchText; } else { - linkData.link = 'http://' + matchText; + link = 'http://' + matchText; } - result.push(linkData); + links.push(link); } urlMatcher.replace(text, replaceFn, this); - return result; -} - -export function extractLinks(text) { - var repRegex = new RegExp('<br>', 'g'); - var matches = testUrlMatch(text.replace(repRegex, '\n')); - - if (!matches.length) { - return {links: null, text: text}; - } - - var links = []; - for (var i = 0; i < matches.length; i++) { - links.push(matches[i].link); - } - - return {links: links, text: text}; + return {links, text}; } export function escapeRegExp(string) { @@ -438,6 +454,8 @@ export function applyTheme(theme) { if (theme.sidebarTextActiveColor) { changeCss('.sidebar--left .nav-pills__container li.active a, .sidebar--left .nav-pills__container li.active a:hover, .sidebar--left .nav-pills__container li.active a:focus, .settings-modal .nav-pills>li.active a, .settings-modal .nav-pills>li.active a:hover, .settings-modal .nav-pills>li.active a:active', 'color:' + theme.sidebarTextActiveColor, 2); changeCss('.sidebar--left .nav li.active a, .sidebar--left .nav li.active a:hover, .sidebar--left .nav li.active a:focus', 'background:' + changeOpacity(theme.sidebarTextActiveColor, 0.1), 1); + changeCss('.search-help-popover .search-autocomplete__item:hover', 'background:' + changeOpacity(theme.sidebarTextActiveColor, 0.05), 1); + changeCss('.search-help-popover .search-autocomplete__item.selected', 'background:' + changeOpacity(theme.sidebarTextActiveColor, 0.15), 1); } if (theme.sidebarHeaderBg) { |