Merge pull request #1218 from hmhealey/underscores
PLT-631/PLT-764/PLT-204/PLT-521 Updating autolinking and handling of underscores by the markdown parser
Этот коммит содержится в:
@@ -6,7 +6,43 @@ const Utils = require('./utils.jsx');
|
|||||||
|
|
||||||
const marked = require('marked');
|
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 = {}) {
|
constructor(options, formattingOptions = {}) {
|
||||||
super(options);
|
super(options);
|
||||||
|
|
||||||
@@ -32,8 +68,20 @@ export class MattermostMarkdownRenderer extends marked.Renderer {
|
|||||||
|
|
||||||
link(href, title, text) {
|
link(href, title, text) {
|
||||||
let outHref = href;
|
let outHref = href;
|
||||||
|
let outText = text;
|
||||||
|
let prefix = '';
|
||||||
|
let suffix = '';
|
||||||
|
|
||||||
if (!(/^(mailto|https?|ftp)/.test(outHref))) {
|
// 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 (!(/[a-z+.-]+:/i).test(outHref)) {
|
||||||
outHref = `http://${outHref}`;
|
outHref = `http://${outHref}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,26 +96,17 @@ export class MattermostMarkdownRenderer extends marked.Renderer {
|
|||||||
output += ' target="_blank">';
|
output += ' target="_blank">';
|
||||||
}
|
}
|
||||||
|
|
||||||
output += text + '</a>';
|
output += outText + '</a>';
|
||||||
|
|
||||||
return output;
|
return prefix + output + suffix;
|
||||||
}
|
}
|
||||||
|
|
||||||
paragraph(text) {
|
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) {
|
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) {
|
table(header, body) {
|
||||||
@@ -78,3 +117,16 @@ export class MattermostMarkdownRenderer extends marked.Renderer {
|
|||||||
return TextFormatting.doFormatText(txt, this.formattingOptions);
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,8 +8,6 @@ const Markdown = require('./markdown.jsx');
|
|||||||
const UserStore = require('../stores/user_store.jsx');
|
const UserStore = require('../stores/user_store.jsx');
|
||||||
const Utils = require('./utils.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
|
// 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
|
// @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:
|
// as part of the second parameter:
|
||||||
@@ -22,11 +20,8 @@ export function formatText(text, options = {}) {
|
|||||||
let output;
|
let output;
|
||||||
|
|
||||||
if (!('markdown' in options) || options.markdown) {
|
if (!('markdown' in options) || options.markdown) {
|
||||||
// the markdown renderer will call doFormatText as necessary so just call marked
|
// the markdown renderer will call doFormatText as necessary
|
||||||
output = marked(text, {
|
output = Markdown.format(text, options);
|
||||||
renderer: new Markdown.MattermostMarkdownRenderer(null, options),
|
|
||||||
sanitize: true
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
output = sanitizeHtml(text);
|
output = sanitizeHtml(text);
|
||||||
output = doFormatText(output, options);
|
output = doFormatText(output, options);
|
||||||
@@ -48,7 +43,7 @@ export function doFormatText(text, options) {
|
|||||||
|
|
||||||
// replace important words and phrases with tokens
|
// replace important words and phrases with tokens
|
||||||
output = autolinkAtMentions(output, tokens);
|
output = autolinkAtMentions(output, tokens);
|
||||||
output = autolinkUrls(output, tokens);
|
output = autolinkEmails(output, tokens);
|
||||||
output = autolinkHashtags(output, tokens);
|
output = autolinkHashtags(output, tokens);
|
||||||
|
|
||||||
if (!('emoticons' in options) || options.emoticon) {
|
if (!('emoticons' in options) || options.emoticon) {
|
||||||
@@ -98,28 +93,21 @@ export function sanitizeHtml(text) {
|
|||||||
return output;
|
return output;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert URLs into tokens
|
// Convert emails into tokens
|
||||||
function autolinkUrls(text, tokens) {
|
function autolinkEmails(text, tokens) {
|
||||||
function replaceUrlWithToken(autolinker, match) {
|
function replaceEmailWithToken(autolinker, match) {
|
||||||
const linkText = match.getMatchedText();
|
const linkText = match.getMatchedText();
|
||||||
let url = linkText;
|
let url = linkText;
|
||||||
|
|
||||||
if (match.getType() === 'email') {
|
if (match.getType() === 'email') {
|
||||||
url = `mailto:${url}`;
|
url = `mailto:${url}`;
|
||||||
} else if (!(/^(mailto|https?|ftp)/.test(url))) {
|
|
||||||
url = `http://${url}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const index = tokens.size;
|
const index = tokens.size;
|
||||||
const alias = `MM_LINK${index}`;
|
const alias = `MM_EMAIL${index}`;
|
||||||
|
|
||||||
var target = 'target="_blank"';
|
|
||||||
if (url.lastIndexOf(Utils.getTeamURLFromAddressBar(), 0) === 0) {
|
|
||||||
target = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
tokens.set(alias, {
|
tokens.set(alias, {
|
||||||
value: `<a class="theme" ${target} href="${url}">${linkText}</a>`,
|
value: `<a class="theme" href="${url}">${linkText}</a>`,
|
||||||
originalText: linkText
|
originalText: linkText
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -128,12 +116,12 @@ function autolinkUrls(text, tokens) {
|
|||||||
|
|
||||||
// we can't just use a static autolinker because we need to set replaceFn
|
// we can't just use a static autolinker because we need to set replaceFn
|
||||||
const autolinker = new Autolinker({
|
const autolinker = new Autolinker({
|
||||||
urls: true,
|
urls: false,
|
||||||
email: true,
|
email: true,
|
||||||
phone: false,
|
phone: false,
|
||||||
twitter: false,
|
twitter: false,
|
||||||
hashtag: false,
|
hashtag: false,
|
||||||
replaceFn: replaceUrlWithToken
|
replaceFn: replaceEmailWithToken
|
||||||
});
|
});
|
||||||
|
|
||||||
return autolinker.link(text);
|
return autolinker.link(text);
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user