MM-62744 Change how custom URLs are autolinked to fix remote user at-mentions (#32080)

* Stop explicitly passing autocompleteUrlSchemes into text formatting code

This is the first part of changing how autocompleteUrlSchemes works so
that it can be moved to be part of the parser like in mobile instead of
happening in the renderer.

I'm not a fan of using the global store directly in utils/markdown, but
this seems like the only way to have this apply to all the Markdown
that's rendered in various helpers throughout the app. Ideally, we'd
have some getMarkdownParser selector and a hook which provides the
config, but that's a future improvement to make"

* MM-62744 Move URL filtering to the Markdown parser instead of the renderer

MM-62744 is caused by two things:

1. URL autolinking takes place in the Markdown parser which occurs
   before at-mention parsing which (despite the "parsing" part) happens
   in the Markdown renderer in the web app.
2. The autolinking in marked is very aggressive and identifies anything
   that looks like some:text as a link.

Those lead to remote mentions like `@user:server` being incorrectly
parsed by Markdown as a link to `user:server`. It isn't renderered as a
link because the URL filtering logic in the Markdown parser blocks that,
but at that point, the Markdown renderer won't check if it's an
at-mention.

By moving the URL filtering to occur earlier, like it does in the mobile
app, the Markdown code won't autolink `@user:server` (unless the server
has `user` configured as a custom URL scheme for some reason), so it's
free to be turned into an at-mention by the renderer code.

* MM-62744 Ensure various regexes and features support remote mentions

* Update marked back to master
Этот коммит содержится в:
Harrison Healey
2025-07-03 11:05:12 -04:00
коммит произвёл GitHub
родитель 0d4c2b72f1
Коммит 5b6320b7dc
22 изменённых файлов: 213 добавлений и 140 удалений

Просмотреть файл

@@ -51,7 +51,7 @@
"lodash": "4.17.21",
"luxon": "3.6.1",
"mark.js": "8.11.1",
"marked": "github:mattermost/marked#3b13ba8ddf725327ddf0298361d6d304a021f2d1",
"marked": "github:mattermost/marked#08f3638e37e17738fafcaf749683ce6fee1d8edc",
"memoize-one": "6.0.0",
"moment-timezone": "0.5.38",
"monaco-editor": "0.52.2",

Просмотреть файл

@@ -173,16 +173,6 @@ exports[`components/drafts/panel/panel_body should have called handleFormattedTe
}
>
<Markdown
autolinkedUrlSchemes={
Array [
"http",
"https",
"ftp",
"mailto",
"tel",
"mattermost",
]
}
channelNamesMap={Object {}}
dispatch={[Function]}
emojiMap={
@@ -405,16 +395,6 @@ exports[`components/drafts/panel/panel_body should match snapshot 1`] = `
}
>
<Markdown
autolinkedUrlSchemes={
Array [
"http",
"https",
"ftp",
"mailto",
"tel",
"mattermost",
]
}
channelNamesMap={Object {}}
dispatch={[Function]}
emojiMap={
@@ -703,16 +683,6 @@ exports[`components/drafts/panel/panel_body should match snapshot for priority 1
}
>
<Markdown
autolinkedUrlSchemes={
Array [
"http",
"https",
"ftp",
"mailto",
"tel",
"mattermost",
]
}
channelNamesMap={Object {}}
dispatch={[Function]}
emojiMap={
@@ -978,16 +948,6 @@ exports[`components/drafts/panel/panel_body should match snapshot for requested_
}
>
<Markdown
autolinkedUrlSchemes={
Array [
"http",
"https",
"ftp",
"mailto",
"tel",
"mattermost",
]
}
channelNamesMap={Object {}}
dispatch={[Function]}
emojiMap={

Просмотреть файл

@@ -6,7 +6,7 @@ import {connect, type ConnectedProps} from 'react-redux';
import {Preferences} from 'mattermost-redux/constants';
import {createSelector} from 'mattermost-redux/selectors/create_selector';
import {getChannelNameToDisplayNameMap} from 'mattermost-redux/selectors/entities/channels';
import {getAutolinkedUrlSchemes, getConfig, getManagedResourcePaths} from 'mattermost-redux/selectors/entities/general';
import {getConfig, getManagedResourcePaths} from 'mattermost-redux/selectors/entities/general';
import {getPost} from 'mattermost-redux/selectors/entities/posts';
import {getBool} from 'mattermost-redux/selectors/entities/preferences';
import {getAllUserMentionKeys} from 'mattermost-redux/selectors/entities/search';
@@ -47,7 +47,6 @@ function makeMapStateToProps() {
}
return {
autolinkedUrlSchemes: getAutolinkedUrlSchemes(state),
channelNamesMap: getChannelNamesMap(state, ownProps),
enableFormatting: getBool(state, Preferences.CATEGORY_ADVANCED_SETTINGS, 'formatting', true),
managedResourcePaths: getManagedResourcePaths(state),

Просмотреть файл

@@ -104,7 +104,6 @@ function Markdown({
userIds,
messageMetadata,
enableFormatting,
autolinkedUrlSchemes,
siteURL,
hasImageProxy,
team,
@@ -124,7 +123,6 @@ function Markdown({
}
const inputOptions = Object.assign({
autolinkedUrlSchemes,
siteURL,
mentionKeys,
highlightKeys,

Просмотреть файл

@@ -128,7 +128,6 @@ class ButtonBinding extends React.PureComponent<Props, State> {
options={{
mentionHighlight: false,
markdown: false,
autolinkedUrlSchemes: [],
}}
/>
</LoadingWrapper>

Просмотреть файл

@@ -18,7 +18,6 @@ exports[`components/post_view/embedded_bindings/embedded_binding should match sn
message="some text"
options={
Object {
"autolinkedUrlSchemes": Array [],
"mentionHighlight": false,
"renderer": LinkOnlyRenderer {
"options": Object {},
@@ -80,7 +79,6 @@ exports[`components/post_view/embedded_bindings/embedded_binding should match sn
message="some text"
options={
Object {
"autolinkedUrlSchemes": Array [],
"mentionHighlight": false,
"renderer": LinkOnlyRenderer {
"options": Object {},
@@ -142,7 +140,6 @@ exports[`components/post_view/embedded_bindings/embedded_binding should match sn
message="some text"
options={
Object {
"autolinkedUrlSchemes": Array [],
"mentionHighlight": false,
"renderer": LinkOnlyRenderer {
"options": Object {},
@@ -204,7 +201,6 @@ exports[`components/post_view/embedded_bindings/embedded_binding should match sn
message="some text"
options={
Object {
"autolinkedUrlSchemes": Array [],
"mentionHighlight": false,
"renderer": LinkOnlyRenderer {
"options": Object {},

Просмотреть файл

@@ -161,7 +161,6 @@ export default class EmbeddedBinding extends React.PureComponent<Props, State> {
options={{
mentionHighlight: false,
renderer: new LinkOnlyRenderer(),
autolinkedUrlSchemes: [],
}}
postId={this.props.post.id}
/>

Просмотреть файл

@@ -26,7 +26,6 @@ const getStatusColors = (theme: Theme) => {
const markdownOptions = {
mentionHighlight: false,
markdown: false,
autolinkedUrlSchemes: [],
};
type Props = {

Просмотреть файл

@@ -425,7 +425,6 @@ exports[`components/post_view/MessageAttachment should match snapshot when the a
options={
Object {
"atMentions": false,
"autolinkedUrlSchemes": Array [],
"mentionHighlight": false,
"renderer": LinkOnlyRenderer {
"options": Object {},
@@ -471,7 +470,6 @@ exports[`components/post_view/MessageAttachment should match snapshot when the a
options={
Object {
"atMentions": false,
"autolinkedUrlSchemes": Array [],
"mentionHighlight": false,
"renderer": LinkOnlyRenderer {
"options": Object {},
@@ -517,7 +515,6 @@ exports[`components/post_view/MessageAttachment should match snapshot when the a
options={
Object {
"atMentions": false,
"autolinkedUrlSchemes": Array [],
"mentionHighlight": false,
"renderer": LinkOnlyRenderer {
"options": Object {},
@@ -563,7 +560,6 @@ exports[`components/post_view/MessageAttachment should match snapshot when the f
options={
Object {
"atMentions": false,
"autolinkedUrlSchemes": Array [],
"mentionHighlight": false,
"renderer": LinkOnlyRenderer {
"options": Object {},

Просмотреть файл

@@ -430,7 +430,6 @@ export default class MessageAttachment extends React.PureComponent<Props, State>
atMentions: false,
mentionHighlight: false,
renderer: new LinkOnlyRenderer(),
autolinkedUrlSchemes: [],
}}
postId={this.props.postId}
/>

Просмотреть файл

@@ -673,6 +673,13 @@ describe('Actions.Posts', () => {
}),
expected: new Set(['ccc', 'ddd', 'fff', 'ggg']),
},
{
name: 'should return potential remote mentions',
input: TestHelper.getPostMock({
message: '@user1:org1 @user2:org2/@user3:org3/@user4:org4 (@user5:org5) @user6:org6',
}),
expected: new Set(['user1:org1', 'user2:org2', 'user3:org3', 'user4:org4', 'user5:org5', 'user6:org6']),
},
];
for (const specialMention of [

Просмотреть файл

@@ -1098,7 +1098,7 @@ export function getNeededAtMentionedUsernamesAndGroups(state: GlobalState, posts
groupsByName = getAllGroupsByName(state);
}
const pattern = /\B@(([a-z0-9_.-]*[a-z0-9_])[.-]*)/gi;
const pattern = /\B@(([a-z0-9.\-_:]*[a-z0-9_])[.\-:]*)/gi;
let match;
while ((match = pattern.exec(text)) !== null) {

Просмотреть файл

@@ -114,6 +114,42 @@ exports[`messageHtmlToComponent Inline markdown image where image is link 1`] =
</div>
`;
exports[`messageHtmlToComponent Remote at mention 1`] = `
<p>
<span
className="mention--highlight"
>
<span
data-mention="joram"
>
<Memo(Connect(Component))
disableGroupHighlight={false}
disableHighlight={false}
mentionName="joram"
>
@joram
</Memo(Connect(Component))>
</span>
</span>
</p>
`;
exports[`messageHtmlToComponent Remote at mention 2`] = `
<p>
<span
data-mention="joram"
>
<Memo(Connect(Component))
disableGroupHighlight={false}
disableHighlight={true}
mentionName="joram"
>
@joram
</Memo(Connect(Component))>
</span>
</p>
`;
exports[`messageHtmlToComponent html 1`] = `
<CodeBlock
code="<div>This is a html div</div>"

Просмотреть файл

@@ -1565,7 +1565,7 @@ export const Constants = {
HERE_MENTION_REGEX: /(?:\B|\b_+)@(here)(?!(\.|-|_)*[^\W_])/gi,
NOTIFY_ALL_MEMBERS: 5,
ALL_MEMBERS_MENTIONS_REGEX: /(?:\B|\b_+)@(channel|all)(?!(\.|-|_)*[^\W_])/gi,
MENTIONS_REGEX: /(?:\B|\b_+)@([a-z0-9.\-_]+)/gi,
MENTIONS_REGEX: /(?:\B|\b_+)@([a-z0-9.\-_]+(?::[a-z0-9.\-_]+)?)/gi,
DEFAULT_CHARACTER_LIMIT: 4000,
IMAGE_TYPE_GIF: 'gif',
TEXT_TYPES: ['txt', 'rtf', 'vtt'],

Просмотреть файл

@@ -3,13 +3,15 @@
import marked from 'marked';
import {getConfig} from 'mattermost-redux/selectors/entities/general';
import {createSelector} from 'mattermost-redux/selectors/create_selector';
import {getAutolinkedUrlSchemes, getConfig} from 'mattermost-redux/selectors/entities/general';
import store from 'stores/redux_store';
import type EmojiMap from 'utils/emoji_map';
import RemoveMarkdown from 'utils/markdown/remove_markdown';
import {convertEntityToCharacter} from 'utils/text_formatting';
import {getScheme} from 'utils/url';
import Renderer from './renderer';
@@ -20,7 +22,9 @@ export function format(text: string, options = {}, emojiMap?: EmojiMap) {
}
export function formatWithRenderer(text: string, renderer: marked.Renderer) {
const config = getConfig(store.getState());
const state = store.getState();
const config = getConfig(state);
const urlFilter = getAutolinkedUrlSchemeFilter(state);
const markdownOptions = {
renderer,
@@ -29,11 +33,24 @@ export function formatWithRenderer(text: string, renderer: marked.Renderer) {
tables: true,
mangle: false,
inlinelatex: config.EnableLatex === 'true' && config.EnableInlineLatex === 'true',
urlFilter,
};
return marked(text, markdownOptions).trim();
}
const getAutolinkedUrlSchemeFilter = createSelector(
'getAutolinkedUrlSchemeFilter',
getAutolinkedUrlSchemes,
(autolinkedUrlSchemes: string[]) => {
return (url: string) => {
const scheme = getScheme(url);
return !scheme || autolinkedUrlSchemes.includes(scheme);
};
},
);
export function stripMarkdown(text: string) {
if (typeof text === 'string' && text.length > 0) {
return convertEntityToCharacter(

Просмотреть файл

@@ -133,7 +133,7 @@ export default class Renderer extends marked.Renderer {
return `<h${level} class="markdown__heading">${text}</h${level}>`;
}
public link(href: string, title: string, text: string, isUrl = false) {
public link(href: string, title: string, text: string) {
let outHref = href;
if (this.formattingOptions.unsafeLinks && mightTriggerExternalRequest(href, this.formattingOptions.siteURL)) {
@@ -147,15 +147,6 @@ export default class Renderer extends marked.Renderer {
const scheme = getScheme(href);
if (!scheme) {
outHref = `http://${outHref}`;
} else if (isUrl && this.formattingOptions.autolinkedUrlSchemes) {
const isValidUrl =
this.formattingOptions.autolinkedUrlSchemes.indexOf(
scheme.toLowerCase(),
) !== -1;
if (!isValidUrl) {
return text;
}
}
}

Просмотреть файл

@@ -135,6 +135,23 @@ const myFunction = () => {
expect(shallow(component).find(AtMention).prop('disableGroupHighlight')).toBe(true);
});
test('Remote at mention', () => {
const options = {mentionHighlight: true, atMentions: true, mentionKeys: [{key: '@joram'}]};
let html = TextFormatting.formatText('@joram', options, emptyEmojiMap);
let component = messageHtmlToComponent(html, {mentionHighlight: true});
expect(component).toMatchSnapshot();
expect(shallow(component).find(AtMention).prop('disableHighlight')).toBe(false);
options.mentionHighlight = false;
html = TextFormatting.formatText('@joram', options, emptyEmojiMap);
component = messageHtmlToComponent(html, {mentionHighlight: false});
expect(component).toMatchSnapshot();
expect(shallow(component).find(AtMention).prop('disableHighlight')).toBe(true);
});
test('typescript', () => {
const input = `Text before typescript codeblock
\`\`\`typescript

Просмотреть файл

@@ -6,6 +6,7 @@ import {createIntl} from 'react-intl';
import {Preferences} from 'mattermost-redux/constants';
import enMessages from 'i18n/en.json';
import {makeInitialState} from 'packages/mattermost-redux/test/test_store';
import {PostListRowListIds, Constants} from 'utils/constants';
import EmojiMap from 'utils/emoji_map';
import * as PostUtils from 'utils/post_utils';
@@ -1354,6 +1355,49 @@ describe('makeGetIsReactionAlreadyAddedToPost', () => {
});
});
describe('makeGetUserOrGroupMentionCountFromMessage', () => {
const baseState = makeInitialState({
entities: {
groups: {
groups: {
group1: TestHelper.getGroupMock({id: 'group1', name: 'group.one', member_count: 4}),
},
},
users: {
profiles: {
remoteUser: TestHelper.getUserMock({id: 'remoteUser', username: 'remote.user:org1'}),
user1: TestHelper.getUserMock({id: 'user1', username: 'user.one'}),
user2: TestHelper.getUserMock({id: 'user2', username: 'user.two'}),
},
},
},
});
test('should count mentioned users', () => {
const getUserOrGroupMentionCountFromMessage = PostUtils.makeGetUserOrGroupMentionCountFromMessage();
expect(getUserOrGroupMentionCountFromMessage(baseState, '@user.one @user.two Hello!')).toEqual(2);
});
test('should count mentioned groups', () => {
const getUserOrGroupMentionCountFromMessage = PostUtils.makeGetUserOrGroupMentionCountFromMessage();
expect(getUserOrGroupMentionCountFromMessage(baseState, '@group.one @user.one Hello!')).toEqual(5);
});
test('should count remote user mentions', () => {
const getUserOrGroupMentionCountFromMessage = PostUtils.makeGetUserOrGroupMentionCountFromMessage();
expect(getUserOrGroupMentionCountFromMessage(baseState, '@user.one @user.two @remote.user:org1 Hello!')).toEqual(3);
});
test('should not count non-existant users/groups', () => {
const getUserOrGroupMentionCountFromMessage = PostUtils.makeGetUserOrGroupMentionCountFromMessage();
expect(getUserOrGroupMentionCountFromMessage(baseState, '@not.user.three @fake.group @not.user:fake Hello!')).toEqual(0);
});
});
describe('makeGetUniqueEmojiNameReactionsForPost', () => {
const baseState = {
entities: {

Просмотреть файл

@@ -17,7 +17,6 @@ import * as Emoticons from './emoticons';
import * as Markdown from './markdown';
const punctuationRegex = /[^\p{L}\d]/u;
const AT_MENTION_PATTERN = /(?:\B|\b_+)@([a-z0-9.\-_]+)/gi;
const UNICODE_EMOJI_REGEX = emojiRegex();
const htmlEmojiPattern = /^<p>\s*(?:<img class="emoticon"[^>]*>|<span data-emoticon[^>]*>[^<]*<\/span>\s*|<span class="emoticon emoticon--unicode">[^<]*<\/span>\s*)+<\/p>$/;
@@ -175,13 +174,6 @@ export interface TextFormattingOptionsBase {
*/
proxyImages: boolean;
/**
* An array of url schemes that will be allowed for autolinking.
*
* Defaults to autolinking with any url scheme.
*/
autolinkedUrlSchemes: string[];
/**
* An array of paths on the server that are managed by another server. Any path provided will be treated as an
* external link that will not by handled by react-router.
@@ -551,17 +543,17 @@ export function autolinkAtMentions(text: string, tokens: Tokens): string {
);
// handle all other mentions (supports trailing punctuation)
let match = output.match(AT_MENTION_PATTERN);
let match = output.match(Constants.MENTIONS_REGEX);
while (match && match.length > 0) {
output = output.replace(AT_MENTION_PATTERN, replaceAtMentionWithToken);
match = output.match(AT_MENTION_PATTERN);
output = output.replace(Constants.MENTIONS_REGEX, replaceAtMentionWithToken);
match = output.match(Constants.MENTIONS_REGEX);
}
return output;
}
export function allAtMentions(text: string): string[] {
return text.match(Constants.SPECIAL_MENTIONS_REGEX && AT_MENTION_PATTERN) || [];
return text.match(Constants.SPECIAL_MENTIONS_REGEX && Constants.MENTIONS_REGEX) || [];
}
export function autolinkChannelMentions(

Просмотреть файл

@@ -147,4 +147,17 @@ describe('TextFormatting.AtMentions', () => {
it(test.label, () => expect(test.actual).toBe(test.expected));
});
});
test('MM-62744 should recognize remote mentions', () => {
expect(TextFormatting.formatText(
'@user1:org1 @user2:org2/@user3:org3/@user4:org4 (@user5:org5) @user6:org6',
{atMentions: true},
emptyEmojiMap,
)).toEqual(
'<p><span data-mention="user1:org1">@user1:org1</span> ' +
'<span data-mention="user2:org2">@user2:org2</span>/<span data-mention="user3:org3">@user3:org3</span>/<span data-mention="user4:org4">@user4:org4</span> ' +
'(<span data-mention="user5:org5">@user5:org5</span>) ' +
'<span data-mention="user6:org6">@user6:org6</span></p>',
);
});
});

Просмотреть файл

@@ -1,6 +1,9 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import store from 'stores/redux_store';
import {makeInitialState} from 'packages/mattermost-redux/test/test_store';
import EmojiMap from 'utils/emoji_map';
import * as Markdown from 'utils/markdown';
import * as TextFormatting from 'utils/text_formatting';
@@ -41,10 +44,6 @@ describe('Markdown.Links', () => {
});
it('External links', () => {
expect(Markdown.format('test.:test').trim()).toBe(
'<p><a class="theme markdown__link" href="test.:test" rel="noreferrer" target="_blank">test.:test</a></p>',
);
expect(Markdown.format('http://example.com').trim()).toBe(
'<p><a class="theme markdown__link" href="http://example.com" rel="noreferrer" target="_blank">http://example.com</a></p>',
);
@@ -402,7 +401,18 @@ describe('Markdown.Links', () => {
});
describe('autolinkedUrlSchemes', () => {
test('all links are rendered when not provided', () => {
test('only some types of links are rendered when there are custom URL schemes defined', () => {
jest.spyOn(store, 'getState').mockReturnValue(makeInitialState({
entities: {
general: {
config: {
CustomUrlSchemes: '',
},
},
},
}));
// These are always linked
expect(Markdown.format('http://example.com').trim()).toBe(`<p>${link('http://example.com')}</p>`);
expect(Markdown.format('https://example.com').trim()).toBe(`<p>${link('https://example.com')}</p>`);
@@ -413,67 +423,68 @@ describe('Markdown.Links', () => {
expect(Markdown.format('mailto:test@example.com').trim()).toBe(`<p>${link('mailto:test@example.com')}</p>`);
// These aren't linked since they're not configured on the server
expect(Markdown.format('git://git.example.com').trim()).toBe('<p>git://git.example.com</p>');
expect(Markdown.format('test:test').trim()).toBe('<p>test:test</p>');
expect(Markdown.format('test.:test').trim()).toBe('<p>test.:test</p>');
expect(Markdown.format('taco+what://example.com').trim()).toBe('<p>taco+what://example.com</p>');
expect(Markdown.format('taco.what://example.com').trim()).toBe('<p>taco.what://example.com</p>');
});
test('matching links are rendered when schemes are provided', () => {
jest.spyOn(store, 'getState').mockReturnValue(makeInitialState({
entities: {
general: {
config: {
CustomUrlSchemes: 'git,test,test.,taco+what,taco.what',
},
},
},
}));
// These are always linked
expect(Markdown.format('http://example.com').trim()).toBe(`<p>${link('http://example.com')}</p>`);
expect(Markdown.format('https://example.com').trim()).toBe(`<p>${link('https://example.com')}</p>`);
expect(Markdown.format('ftp://ftp.example.com').trim()).toBe(`<p>${link('ftp://ftp.example.com')}</p>`);
expect(Markdown.format('tel:1-555-123-4567').trim()).toBe(`<p>${link('tel:1-555-123-4567')}</p>`);
expect(Markdown.format('mailto:test@example.com').trim()).toBe(`<p>${link('mailto:test@example.com')}</p>`);
// These are linked since they're configured on the server
expect(Markdown.format('git://git.example.com').trim()).toBe(`<p>${link('git://git.example.com')}</p>`);
expect(Markdown.format('test:test').trim()).toBe(`<p>${link('test:test')}</p>`);
});
test('no links are rendered when no schemes are provided', () => {
const options = {
autolinkedUrlSchemes: [],
};
expect(Markdown.format('test.:test').trim()).toBe(`<p>${link('test.:test')}</p>`);
expect(Markdown.format('http://example.com', options).trim()).toBe('<p>http://example.com</p>');
expect(Markdown.format('taco+what://example.com').trim()).toBe(`<p>${link('taco+what://example.com')}</p>`);
expect(Markdown.format('https://example.com', options).trim()).toBe('<p>https://example.com</p>');
expect(Markdown.format('ftp://ftp.example.com', options).trim()).toBe('<p>ftp://ftp.example.com</p>');
expect(Markdown.format('tel:1-555-123-4567', options).trim()).toBe('<p>tel:1-555-123-4567</p>');
expect(Markdown.format('mailto:test@example.com', options).trim()).toBe('<p>mailto:test@example.com</p>');
expect(Markdown.format('git://git.example.com', options).trim()).toBe('<p>git://git.example.com</p>');
expect(Markdown.format('test:test', options).trim()).toBe('<p>test:test</p>');
});
test('only matching links are rendered when schemes are provided', () => {
const options = {
autolinkedUrlSchemes: ['https', 'git', 'test', 'test.', 'taco+what', 'taco.what'],
};
expect(Markdown.format('http://example.com', options).trim()).toBe('<p>http://example.com</p>');
expect(Markdown.format('https://example.com', options).trim()).toBe(`<p>${link('https://example.com')}</p>`);
expect(Markdown.format('ftp://ftp.example.com', options).trim()).toBe('<p>ftp://ftp.example.com</p>');
expect(Markdown.format('tel:1-555-123-4567', options).trim()).toBe('<p>tel:1-555-123-4567</p>');
expect(Markdown.format('mailto:test@example.com', options).trim()).toBe('<p>mailto:test@example.com</p>');
expect(Markdown.format('git://git.example.com', options).trim()).toBe(`<p>${link('git://git.example.com')}</p>`);
expect(Markdown.format('test:test', options).trim()).toBe(`<p>${link('test:test')}</p>`);
expect(Markdown.format('test.:test', options).trim()).toBe(`<p>${link('test.:test')}</p>`);
expect(Markdown.format('taco+what://example.com', options).trim()).toBe(`<p>${link('taco+what://example.com')}</p>`);
expect(Markdown.format('taco.what://example.com', options).trim()).toBe(`<p>${link('taco.what://example.com')}</p>`);
expect(Markdown.format('taco.what://example.com').trim()).toBe(`<p>${link('taco.what://example.com')}</p>`);
});
test('explicit links are not affected by this setting', () => {
const options = {
autolinkedUrlSchemes: [],
};
jest.spyOn(store, 'getState').mockReturnValue(makeInitialState({
entities: {
general: {
config: {
CustomUrlSchemes: '',
},
},
},
}));
expect(Markdown.format('www.example.com', options).trim()).toBe(`<p>${link('http://www.example.com', 'www.example.com')}</p>`);
expect(Markdown.format('www.example.com').trim()).toBe(`<p>${link('http://www.example.com', 'www.example.com')}</p>`);
expect(Markdown.format('[link](git://git.example.com)', options).trim()).toBe(`<p>${link('git://git.example.com', 'link')}</p>`);
expect(Markdown.format('[link](git://git.example.com)').trim()).toBe(`<p>${link('git://git.example.com', 'link')}</p>`);
expect(Markdown.format('<http://example.com>', options).trim()).toBe(`<p>${link('http://example.com')}</p>`);
expect(Markdown.format('<git://git.example.com>').trim()).toBe(`<p>${link('git://git.example.com')}</p>`);
});
});
});

6
webapp/package-lock.json сгенерированный
Просмотреть файл

@@ -105,7 +105,7 @@
"lodash": "4.17.21",
"luxon": "3.6.1",
"mark.js": "8.11.1",
"marked": "github:mattermost/marked#3b13ba8ddf725327ddf0298361d6d304a021f2d1",
"marked": "github:mattermost/marked#08f3638e37e17738fafcaf749683ce6fee1d8edc",
"memoize-one": "6.0.0",
"moment-timezone": "0.5.38",
"monaco-editor": "0.52.2",
@@ -20662,8 +20662,8 @@
},
"node_modules/marked": {
"version": "0.3.6",
"resolved": "git+ssh://git@github.com/mattermost/marked.git#3b13ba8ddf725327ddf0298361d6d304a021f2d1",
"integrity": "sha512-/G1/szBhwdamaCQQjdk+eKQKw4Xkmk8aQYxEQvIjfbcFHEaIgUbpH6RBSC+b0pDzlhm7mxHAaGnzfH3WqUks1Q==",
"resolved": "git+ssh://git@github.com/mattermost/marked.git#08f3638e37e17738fafcaf749683ce6fee1d8edc",
"integrity": "sha512-zaU3iMz9BuvdYUjAF4zL0ioAstBgFI+xUgO5BNaEDGI+cXoXcRa5X2Bq0uA7Xn6NSQi0X04Xn5EuH+pxhvIllQ==",
"license": "MIT",
"bin": {
"marked": "bin/marked"