Refactor matrix-linkify module (#7279)

Refactor the module to make it easier for upgrade and proper separation of code contexts
This commit is contained in:
Dariusz Niemczyk 2021-12-03 15:00:56 +01:00 committed by GitHub
parent 3b9e39ffca
commit 961fec9081
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 99 additions and 206 deletions

View file

@ -20,9 +20,7 @@ limitations under the License.
import React, { ReactNode } from 'react'; import React, { ReactNode } from 'react';
import sanitizeHtml from 'sanitize-html'; import sanitizeHtml from 'sanitize-html';
import cheerio from 'cheerio'; import cheerio from 'cheerio';
import * as linkify from 'linkifyjs'; import { _linkifyElement, _linkifyString } from './linkify-matrix';
import _linkifyElement from 'linkifyjs/element';
import _linkifyString from 'linkifyjs/string';
import classNames from 'classnames'; import classNames from 'classnames';
import EMOJIBASE_REGEX from 'emojibase-regex'; import EMOJIBASE_REGEX from 'emojibase-regex';
import katex from 'katex'; import katex from 'katex';
@ -30,14 +28,12 @@ import { AllHtmlEntities } from 'html-entities';
import { IContent } from 'matrix-js-sdk/src/models/event'; import { IContent } from 'matrix-js-sdk/src/models/event';
import { IExtendedSanitizeOptions } from './@types/sanitize-html'; import { IExtendedSanitizeOptions } from './@types/sanitize-html';
import linkifyMatrix from './linkify-matrix';
import SettingsStore from './settings/SettingsStore'; import SettingsStore from './settings/SettingsStore';
import { tryTransformPermalinkToLocalHref } from "./utils/permalinks/Permalinks"; import { tryTransformPermalinkToLocalHref } from "./utils/permalinks/Permalinks";
import { getEmojiFromUnicode } from "./emoji"; import { getEmojiFromUnicode } from "./emoji";
import ReplyChain from "./components/views/elements/ReplyChain"; import ReplyChain from "./components/views/elements/ReplyChain";
import { mediaFromMxc } from "./customisations/Media"; import { mediaFromMxc } from "./customisations/Media";
import { ELEMENT_URL_PATTERN, options as linkifyMatrixOptions } from './linkify-matrix';
linkifyMatrix(linkify);
// Anything outside the basic multilingual plane will be a surrogate pair // Anything outside the basic multilingual plane will be a surrogate pair
const SURROGATE_PAIR_PATTERN = /([\ud800-\udbff])([\udc00-\udfff])/; const SURROGATE_PAIR_PATTERN = /([\ud800-\udbff])([\udc00-\udfff])/;
@ -180,7 +176,7 @@ const transformTags: IExtendedSanitizeOptions["transformTags"] = { // custom to
attribs.target = '_blank'; // by default attribs.target = '_blank'; // by default
const transformed = tryTransformPermalinkToLocalHref(attribs.href); const transformed = tryTransformPermalinkToLocalHref(attribs.href);
if (transformed !== attribs.href || attribs.href.match(linkifyMatrix.ELEMENT_URL_PATTERN)) { if (transformed !== attribs.href || attribs.href.match(ELEMENT_URL_PATTERN)) {
attribs.href = transformed; attribs.href = transformed;
delete attribs.target; delete attribs.target;
} }
@ -537,10 +533,10 @@ export function bodyToHtml(content: IContent, highlights: string[], opts: IOpts
* Linkifies the given string. This is a wrapper around 'linkifyjs/string'. * Linkifies the given string. This is a wrapper around 'linkifyjs/string'.
* *
* @param {string} str string to linkify * @param {string} str string to linkify
* @param {object} [options] Options for linkifyString. Default: linkifyMatrix.options * @param {object} [options] Options for linkifyString. Default: linkifyMatrixOptions
* @returns {string} Linkified string * @returns {string} Linkified string
*/ */
export function linkifyString(str: string, options = linkifyMatrix.options): string { export function linkifyString(str: string, options = linkifyMatrixOptions): string {
return _linkifyString(str, options); return _linkifyString(str, options);
} }
@ -548,10 +544,10 @@ export function linkifyString(str: string, options = linkifyMatrix.options): str
* Linkifies the given DOM element. This is a wrapper around 'linkifyjs/element'. * Linkifies the given DOM element. This is a wrapper around 'linkifyjs/element'.
* *
* @param {object} element DOM element to linkify * @param {object} element DOM element to linkify
* @param {object} [options] Options for linkifyElement. Default: linkifyMatrix.options * @param {object} [options] Options for linkifyElement. Default: linkifyMatrixOptions
* @returns {object} * @returns {object}
*/ */
export function linkifyElement(element: HTMLElement, options = linkifyMatrix.options): HTMLElement { export function linkifyElement(element: HTMLElement, options = linkifyMatrixOptions): HTMLElement {
return _linkifyElement(element, options); return _linkifyElement(element, options);
} }
@ -559,10 +555,10 @@ export function linkifyElement(element: HTMLElement, options = linkifyMatrix.opt
* Linkify the given string and sanitize the HTML afterwards. * Linkify the given string and sanitize the HTML afterwards.
* *
* @param {string} dirtyHtml The HTML string to sanitize and linkify * @param {string} dirtyHtml The HTML string to sanitize and linkify
* @param {object} [options] Options for linkifyString. Default: linkifyMatrix.options * @param {object} [options] Options for linkifyString. Default: linkifyMatrixOptions
* @returns {string} * @returns {string}
*/ */
export function linkifyAndSanitizeHtml(dirtyHtml: string, options = linkifyMatrix.options): string { export function linkifyAndSanitizeHtml(dirtyHtml: string, options = linkifyMatrixOptions): string {
return sanitizeHtml(linkifyString(dirtyHtml, options), sanitizeHtmlParams); return sanitizeHtml(linkifyString(dirtyHtml, options), sanitizeHtmlParams);
} }

View file

@ -18,7 +18,7 @@ limitations under the License.
import * as commonmark from 'commonmark'; import * as commonmark from 'commonmark';
import { escape } from "lodash"; import { escape } from "lodash";
import { logger } from 'matrix-js-sdk/src/logger'; import { logger } from 'matrix-js-sdk/src/logger';
import * as linkify from 'linkifyjs'; import { linkify } from './linkify-matrix';
const ALLOWED_HTML_TAGS = ['sub', 'sup', 'del', 'u']; const ALLOWED_HTML_TAGS = ['sub', 'sup', 'del', 'u'];

View file

@ -17,7 +17,6 @@ limitations under the License.
import React, { ComponentType, createRef } from 'react'; import React, { ComponentType, createRef } from 'react';
import { createClient } from "matrix-js-sdk/src/matrix"; import { createClient } from "matrix-js-sdk/src/matrix";
import { InvalidStoreError } from "matrix-js-sdk/src/errors"; import { InvalidStoreError } from "matrix-js-sdk/src/errors";
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { sleep, defer, IDeferred, QueryDict } from "matrix-js-sdk/src/utils"; import { sleep, defer, IDeferred, QueryDict } from "matrix-js-sdk/src/utils";
@ -38,7 +37,6 @@ import Notifier from '../../Notifier';
import Modal from "../../Modal"; import Modal from "../../Modal";
import { showRoomInviteDialog, showStartChatInviteDialog } from '../../RoomInvite'; import { showRoomInviteDialog, showStartChatInviteDialog } from '../../RoomInvite';
import * as Rooms from '../../Rooms'; import * as Rooms from '../../Rooms';
import linkifyMatrix from "../../linkify-matrix";
import * as Lifecycle from '../../Lifecycle'; import * as Lifecycle from '../../Lifecycle';
// LifecycleStore is not used but does listen to and dispatch actions // LifecycleStore is not used but does listen to and dispatch actions
import '../../stores/LifecycleStore'; import '../../stores/LifecycleStore';
@ -59,7 +57,6 @@ import { storeRoomAliasInCache } from '../../RoomAliasCache';
import ToastStore from "../../stores/ToastStore"; import ToastStore from "../../stores/ToastStore";
import * as StorageManager from "../../utils/StorageManager"; import * as StorageManager from "../../utils/StorageManager";
import type LoggedInViewType from "./LoggedInView"; import type LoggedInViewType from "./LoggedInView";
import { ViewUserPayload } from "../../dispatcher/payloads/ViewUserPayload";
import { Action } from "../../dispatcher/actions"; import { Action } from "../../dispatcher/actions";
import { import {
showToast as showAnalyticsToast, showToast as showAnalyticsToast,
@ -347,18 +344,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
// we don't do it as react state as i'm scared about triggering needless react refreshes. // we don't do it as react state as i'm scared about triggering needless react refreshes.
this.subTitleStatus = ''; this.subTitleStatus = '';
// this can technically be done anywhere but doing this here keeps all
// the routing url path logic together.
if (this.onAliasClick) {
linkifyMatrix.onAliasClick = this.onAliasClick;
}
if (this.onUserClick) {
linkifyMatrix.onUserClick = this.onUserClick;
}
if (this.onGroupClick) {
linkifyMatrix.onGroupClick = this.onGroupClick;
}
// the first thing to do is to try the token params in the query-string // the first thing to do is to try the token params in the query-string
// if the session isn't soft logged out (ie: is a clean session being logged in) // if the session isn't soft logged out (ie: is a clean session being logged in)
if (!Lifecycle.isSoftLogout()) { if (!Lifecycle.isSoftLogout()) {
@ -1898,28 +1883,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
} }
this.setPageSubtitle(); this.setPageSubtitle();
} }
onAliasClick(event: MouseEvent, alias: string) {
event.preventDefault();
dis.dispatch({ action: Action.ViewRoom, room_alias: alias });
}
onUserClick(event: MouseEvent, userId: string) {
event.preventDefault();
const member = new RoomMember(null, userId);
if (!member) { return; }
dis.dispatch<ViewUserPayload>({
action: Action.ViewUser,
member: member,
});
}
onGroupClick(event: MouseEvent, groupId: string) {
event.preventDefault();
dis.dispatch({ action: 'view_group', group_id: groupId });
}
onLogoutClick(event: React.MouseEvent<HTMLAnchorElement, MouseEvent>) { onLogoutClick(event: React.MouseEvent<HTMLAnchorElement, MouseEvent>) {
dis.dispatch({ dis.dispatch({
action: 'logout', action: 'logout',

View file

@ -15,12 +15,19 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import * as linkifyjs from 'linkifyjs';
import linkifyElement from 'linkifyjs/element';
import linkifyString from 'linkifyjs/string';
import { RoomMember } from 'matrix-js-sdk/src/models/room-member';
import { baseUrl } from "./utils/permalinks/SpecPermalinkConstructor"; import { baseUrl } from "./utils/permalinks/SpecPermalinkConstructor";
import { import {
parsePermalink, parsePermalink,
tryTransformEntityToPermalink, tryTransformEntityToPermalink,
tryTransformPermalinkToLocalHref, tryTransformPermalinkToLocalHref,
} from "./utils/permalinks/Permalinks"; } from "./utils/permalinks/Permalinks";
import dis from './dispatcher/dispatcher';
import { Action } from './dispatcher/actions';
import { ViewUserPayload } from './dispatcher/payloads/ViewUserPayload';
enum Type { enum Type {
URL = "url", URL = "url",
@ -29,7 +36,20 @@ enum Type {
GroupId = "groupid" GroupId = "groupid"
} }
function matrixLinkify(linkify): void { // Linkifyjs types don't have parser, which really makes this harder.
const linkifyTokens = (linkifyjs as any).scanner.TOKENS;
enum MatrixLinkInitialToken {
POUND = linkifyTokens.POUND,
PLUS = linkifyTokens.PLUS,
AT = linkifyTokens.AT,
}
/**
* Token should be one of the type of linkify.parser.TOKENS[AT | PLUS | POUND]
* but due to typing issues it's just not a feasible solution.
* This problem kind of gets solved in linkify 3.0
*/
function parseFreeformMatrixLinks(linkify, token: MatrixLinkInitialToken, type: Type): void {
// Text tokens // Text tokens
const TT = linkify.scanner.TOKENS; const TT = linkify.scanner.TOKENS;
// Multi tokens // Multi tokens
@ -37,152 +57,70 @@ function matrixLinkify(linkify): void {
const MultiToken = MT.Base; const MultiToken = MT.Base;
const S_START = linkify.parser.start; const S_START = linkify.parser.start;
if (TT.UNDERSCORE === undefined) { const TOKEN = function(value) {
throw new Error("linkify-matrix requires linkifyjs 2.1.1: this version is too old.");
}
const ROOMALIAS = function(value) {
MultiToken.call(this, value); MultiToken.call(this, value);
this.type = 'roomalias'; this.type = type;
this.isLink = true; this.isLink = true;
}; };
ROOMALIAS.prototype = new MultiToken(); TOKEN.prototype = new MultiToken();
const S_HASH = S_START.jump(TT.POUND); const S_TOKEN = S_START.jump(token);
const S_HASH_NAME = new linkify.parser.State(); const S_TOKEN_NAME = new linkify.parser.State();
const S_HASH_NAME_COLON = new linkify.parser.State(); const S_TOKEN_NAME_COLON = new linkify.parser.State();
const S_HASH_NAME_COLON_DOMAIN = new linkify.parser.State(ROOMALIAS); const S_TOKEN_NAME_COLON_DOMAIN = new linkify.parser.State(TOKEN);
const S_HASH_NAME_COLON_DOMAIN_DOT = new linkify.parser.State(); const S_TOKEN_NAME_COLON_DOMAIN_DOT = new linkify.parser.State();
const S_ROOMALIAS = new linkify.parser.State(ROOMALIAS); const S_MX_LINK = new linkify.parser.State(TOKEN);
const S_ROOMALIAS_COLON = new linkify.parser.State(); const S_MX_LINK_COLON = new linkify.parser.State();
const S_ROOMALIAS_COLON_NUM = new linkify.parser.State(ROOMALIAS); const S_MX_LINK_COLON_NUM = new linkify.parser.State(TOKEN);
const roomnameTokens = [ const allowedFreeformTokens = [
TT.DOT, TT.DOT,
TT.PLUS, TT.PLUS,
TT.NUM, TT.NUM,
TT.DOMAIN, TT.DOMAIN,
TT.TLD, TT.TLD,
TT.UNDERSCORE, TT.UNDERSCORE,
TT.POUND, token,
// because 'localhost' is tokenised to the localhost token, // because 'localhost' is tokenised to the localhost token,
// usernames @localhost:foo.com are otherwise not matched! // usernames @localhost:foo.com are otherwise not matched!
TT.LOCALHOST, TT.LOCALHOST,
]; ];
S_HASH.on(roomnameTokens, S_HASH_NAME); S_TOKEN.on(allowedFreeformTokens, S_TOKEN_NAME);
S_HASH_NAME.on(roomnameTokens, S_HASH_NAME); S_TOKEN_NAME.on(allowedFreeformTokens, S_TOKEN_NAME);
S_HASH_NAME.on(TT.DOMAIN, S_HASH_NAME); S_TOKEN_NAME.on(TT.DOMAIN, S_TOKEN_NAME);
S_HASH_NAME.on(TT.COLON, S_HASH_NAME_COLON); S_TOKEN_NAME.on(TT.COLON, S_TOKEN_NAME_COLON);
S_HASH_NAME_COLON.on(TT.DOMAIN, S_HASH_NAME_COLON_DOMAIN); S_TOKEN_NAME_COLON.on(TT.DOMAIN, S_TOKEN_NAME_COLON_DOMAIN);
S_HASH_NAME_COLON.on(TT.LOCALHOST, S_ROOMALIAS); // accept #foo:localhost S_TOKEN_NAME_COLON.on(TT.LOCALHOST, S_MX_LINK); // accept #foo:localhost
S_HASH_NAME_COLON.on(TT.TLD, S_ROOMALIAS); // accept #foo:com (mostly for (TLD|DOMAIN)+ mixing) S_TOKEN_NAME_COLON.on(TT.TLD, S_MX_LINK); // accept #foo:com (mostly for (TLD|DOMAIN)+ mixing)
S_HASH_NAME_COLON_DOMAIN.on(TT.DOT, S_HASH_NAME_COLON_DOMAIN_DOT); S_TOKEN_NAME_COLON_DOMAIN.on(TT.DOT, S_TOKEN_NAME_COLON_DOMAIN_DOT);
S_HASH_NAME_COLON_DOMAIN_DOT.on(TT.DOMAIN, S_HASH_NAME_COLON_DOMAIN); S_TOKEN_NAME_COLON_DOMAIN_DOT.on(TT.DOMAIN, S_TOKEN_NAME_COLON_DOMAIN);
S_HASH_NAME_COLON_DOMAIN_DOT.on(TT.TLD, S_ROOMALIAS); S_TOKEN_NAME_COLON_DOMAIN_DOT.on(TT.TLD, S_MX_LINK);
S_ROOMALIAS.on(TT.DOT, S_HASH_NAME_COLON_DOMAIN_DOT); // accept repeated TLDs (e.g .org.uk) S_MX_LINK.on(TT.DOT, S_TOKEN_NAME_COLON_DOMAIN_DOT); // accept repeated TLDs (e.g .org.uk)
S_ROOMALIAS.on(TT.COLON, S_ROOMALIAS_COLON); // do not accept trailing `:` S_MX_LINK.on(TT.COLON, S_MX_LINK_COLON); // do not accept trailing `:`
S_ROOMALIAS_COLON.on(TT.NUM, S_ROOMALIAS_COLON_NUM); // but do accept :NUM (port specifier) S_MX_LINK_COLON.on(TT.NUM, S_MX_LINK_COLON_NUM); // but do accept :NUM (port specifier)
const USERID = function(value) {
MultiToken.call(this, value);
this.type = 'userid';
this.isLink = true;
};
USERID.prototype = new MultiToken();
const S_AT = S_START.jump(TT.AT);
const S_AT_NAME = new linkify.parser.State();
const S_AT_NAME_COLON = new linkify.parser.State();
const S_AT_NAME_COLON_DOMAIN = new linkify.parser.State(USERID);
const S_AT_NAME_COLON_DOMAIN_DOT = new linkify.parser.State();
const S_USERID = new linkify.parser.State(USERID);
const S_USERID_COLON = new linkify.parser.State();
const S_USERID_COLON_NUM = new linkify.parser.State(USERID);
const usernameTokens = [
TT.DOT,
TT.UNDERSCORE,
TT.PLUS,
TT.NUM,
TT.DOMAIN,
TT.TLD,
// as in roomnameTokens
TT.LOCALHOST,
];
S_AT.on(usernameTokens, S_AT_NAME);
S_AT_NAME.on(usernameTokens, S_AT_NAME);
S_AT_NAME.on(TT.DOMAIN, S_AT_NAME);
S_AT_NAME.on(TT.COLON, S_AT_NAME_COLON);
S_AT_NAME_COLON.on(TT.DOMAIN, S_AT_NAME_COLON_DOMAIN);
S_AT_NAME_COLON.on(TT.LOCALHOST, S_USERID); // accept @foo:localhost
S_AT_NAME_COLON.on(TT.TLD, S_USERID); // accept @foo:com (mostly for (TLD|DOMAIN)+ mixing)
S_AT_NAME_COLON_DOMAIN.on(TT.DOT, S_AT_NAME_COLON_DOMAIN_DOT);
S_AT_NAME_COLON_DOMAIN_DOT.on(TT.DOMAIN, S_AT_NAME_COLON_DOMAIN);
S_AT_NAME_COLON_DOMAIN_DOT.on(TT.TLD, S_USERID);
S_USERID.on(TT.DOT, S_AT_NAME_COLON_DOMAIN_DOT); // accept repeated TLDs (e.g .org.uk)
S_USERID.on(TT.COLON, S_USERID_COLON); // do not accept trailing `:`
S_USERID_COLON.on(TT.NUM, S_USERID_COLON_NUM); // but do accept :NUM (port specifier)
const GROUPID = function(value) {
MultiToken.call(this, value);
this.type = 'groupid';
this.isLink = true;
};
GROUPID.prototype = new MultiToken();
const S_PLUS = S_START.jump(TT.PLUS);
const S_PLUS_NAME = new linkify.parser.State();
const S_PLUS_NAME_COLON = new linkify.parser.State();
const S_PLUS_NAME_COLON_DOMAIN = new linkify.parser.State(GROUPID);
const S_PLUS_NAME_COLON_DOMAIN_DOT = new linkify.parser.State();
const S_GROUPID = new linkify.parser.State(GROUPID);
const S_GROUPID_COLON = new linkify.parser.State();
const S_GROUPID_COLON_NUM = new linkify.parser.State(GROUPID);
const groupIdTokens = [
TT.DOT,
TT.UNDERSCORE,
TT.PLUS,
TT.NUM,
TT.DOMAIN,
TT.TLD,
// as in roomnameTokens
TT.LOCALHOST,
];
S_PLUS.on(groupIdTokens, S_PLUS_NAME);
S_PLUS_NAME.on(groupIdTokens, S_PLUS_NAME);
S_PLUS_NAME.on(TT.DOMAIN, S_PLUS_NAME);
S_PLUS_NAME.on(TT.COLON, S_PLUS_NAME_COLON);
S_PLUS_NAME_COLON.on(TT.DOMAIN, S_PLUS_NAME_COLON_DOMAIN);
S_PLUS_NAME_COLON.on(TT.LOCALHOST, S_GROUPID); // accept +foo:localhost
S_PLUS_NAME_COLON.on(TT.TLD, S_GROUPID); // accept +foo:com (mostly for (TLD|DOMAIN)+ mixing)
S_PLUS_NAME_COLON_DOMAIN.on(TT.DOT, S_PLUS_NAME_COLON_DOMAIN_DOT);
S_PLUS_NAME_COLON_DOMAIN_DOT.on(TT.DOMAIN, S_PLUS_NAME_COLON_DOMAIN);
S_PLUS_NAME_COLON_DOMAIN_DOT.on(TT.TLD, S_GROUPID);
S_GROUPID.on(TT.DOT, S_PLUS_NAME_COLON_DOMAIN_DOT); // accept repeated TLDs (e.g .org.uk)
S_GROUPID.on(TT.COLON, S_GROUPID_COLON); // do not accept trailing `:`
S_GROUPID_COLON.on(TT.NUM, S_GROUPID_COLON_NUM); // but do accept :NUM (port specifier)
} }
// stubs, overwritten in MatrixChat's componentDidMount function onUserClick(event: MouseEvent, userId: string) {
matrixLinkify.onUserClick = function(e: MouseEvent, userId: string) { e.preventDefault(); }; const member = new RoomMember(null, userId);
matrixLinkify.onAliasClick = function(e: MouseEvent, roomAlias: string) { e.preventDefault(); }; if (!member) { return; }
matrixLinkify.onGroupClick = function(e: MouseEvent, groupId: string) { e.preventDefault(); }; dis.dispatch<ViewUserPayload>({
action: Action.ViewUser,
member: member,
});
}
function onAliasClick(event: MouseEvent, roomAlias: string) {
event.preventDefault();
dis.dispatch({ action: 'view_room', room_alias: roomAlias });
}
function onGroupClick(event: MouseEvent, groupId: string) {
event.preventDefault();
dis.dispatch({ action: 'view_group', group_id: groupId });
}
const escapeRegExp = function(string): string { const escapeRegExp = function(string): string {
return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@ -190,19 +128,19 @@ const escapeRegExp = function(string): string {
// Recognise URLs from both our local and official Element deployments. // Recognise URLs from both our local and official Element deployments.
// Anyone else really should be using matrix.to. // Anyone else really should be using matrix.to.
matrixLinkify.ELEMENT_URL_PATTERN = export const ELEMENT_URL_PATTERN =
"^(?:https?://)?(?:" + "^(?:https?://)?(?:" +
escapeRegExp(window.location.host + window.location.pathname) + "|" + escapeRegExp(window.location.host + window.location.pathname) + "|" +
"(?:www\\.)?(?:riot|vector)\\.im/(?:app|beta|staging|develop)/|" + "(?:www\\.)?(?:riot|vector)\\.im/(?:app|beta|staging|develop)/|" +
"(?:app|beta|staging|develop)\\.element\\.io/" + "(?:app|beta|staging|develop)\\.element\\.io/" +
")(#.*)"; ")(#.*)";
matrixLinkify.MATRIXTO_URL_PATTERN = "^(?:https?://)?(?:www\\.)?matrix\\.to/#/(([#@!+]).*)"; export const MATRIXTO_URL_PATTERN = "^(?:https?://)?(?:www\\.)?matrix\\.to/#/(([#@!+]).*)";
matrixLinkify.MATRIXTO_MD_LINK_PATTERN = export const MATRIXTO_MD_LINK_PATTERN =
'\\[([^\\]]*)\\]\\((?:https?://)?(?:www\\.)?matrix\\.to/#/([#@!+][^\\)]*)\\)'; '\\[([^\\]]*)\\]\\((?:https?://)?(?:www\\.)?matrix\\.to/#/([#@!+][^\\)]*)\\)';
matrixLinkify.MATRIXTO_BASE_URL= baseUrl; export const MATRIXTO_BASE_URL= baseUrl;
matrixLinkify.options = { export const options = {
events: function(href: string, type: Type | string): Partial<GlobalEventHandlers> { events: function(href: string, type: Type | string): Partial<GlobalEventHandlers> {
switch (type) { switch (type) {
case Type.URL: { case Type.URL: {
@ -213,7 +151,7 @@ matrixLinkify.options = {
return { return {
// @ts-ignore see https://linkify.js.org/docs/options.html // @ts-ignore see https://linkify.js.org/docs/options.html
click: function(e) { click: function(e) {
matrixLinkify.onUserClick(e, permalink.userId); onUserClick(e, permalink.userId);
}, },
}; };
} }
@ -226,21 +164,21 @@ matrixLinkify.options = {
return { return {
// @ts-ignore see https://linkify.js.org/docs/options.html // @ts-ignore see https://linkify.js.org/docs/options.html
click: function(e) { click: function(e) {
matrixLinkify.onUserClick(e, href); onUserClick(e, href);
}, },
}; };
case Type.RoomAlias: case Type.RoomAlias:
return { return {
// @ts-ignore see https://linkify.js.org/docs/options.html // @ts-ignore see https://linkify.js.org/docs/options.html
click: function(e) { click: function(e) {
matrixLinkify.onAliasClick(e, href); onAliasClick(e, href);
}, },
}; };
case Type.GroupId: case Type.GroupId:
return { return {
// @ts-ignore see https://linkify.js.org/docs/options.html // @ts-ignore see https://linkify.js.org/docs/options.html
click: function(e) { click: function(e) {
matrixLinkify.onGroupClick(e, href); onGroupClick(e, href);
}, },
}; };
} }
@ -265,7 +203,7 @@ matrixLinkify.options = {
if (type === Type.URL) { if (type === Type.URL) {
try { try {
const transformed = tryTransformPermalinkToLocalHref(href); const transformed = tryTransformPermalinkToLocalHref(href);
if (transformed !== href || decodeURIComponent(href).match(matrixLinkify.ELEMENT_URL_PATTERN)) { if (transformed !== href || decodeURIComponent(href).match(ELEMENT_URL_PATTERN)) {
return null; return null;
} else { } else {
return '_blank'; return '_blank';
@ -278,4 +216,14 @@ matrixLinkify.options = {
}, },
}; };
export default matrixLinkify; // Run the plugins
// Linkify room aliases
parseFreeformMatrixLinks(linkifyjs, MatrixLinkInitialToken.POUND, Type.RoomAlias);
// Linkify group IDs
parseFreeformMatrixLinks(linkifyjs, MatrixLinkInitialToken.PLUS, Type.GroupId);
// Linkify user IDs
parseFreeformMatrixLinks(linkifyjs, MatrixLinkInitialToken.AT, Type.UserId);
export const linkify = linkifyjs;
export const _linkifyElement = linkifyElement;
export const _linkifyString = linkifyString;

View file

@ -25,10 +25,10 @@ import { MatrixClientPeg } from "../../MatrixClientPeg";
import SpecPermalinkConstructor, { baseUrl as matrixtoBaseUrl } from "./SpecPermalinkConstructor"; import SpecPermalinkConstructor, { baseUrl as matrixtoBaseUrl } from "./SpecPermalinkConstructor";
import PermalinkConstructor, { PermalinkParts } from "./PermalinkConstructor"; import PermalinkConstructor, { PermalinkParts } from "./PermalinkConstructor";
import ElementPermalinkConstructor from "./ElementPermalinkConstructor"; import ElementPermalinkConstructor from "./ElementPermalinkConstructor";
import matrixLinkify from "../../linkify-matrix";
import SdkConfig from "../../SdkConfig"; import SdkConfig from "../../SdkConfig";
import { logger } from "matrix-js-sdk/src/logger"; import { logger } from "matrix-js-sdk/src/logger";
import { ELEMENT_URL_PATTERN } from "../../linkify-matrix";
// The maximum number of servers to pick when working out which servers // The maximum number of servers to pick when working out which servers
// to add to permalinks. The servers are appended as ?via=example.org // to add to permalinks. The servers are appended as ?via=example.org
@ -348,7 +348,7 @@ export function tryTransformPermalinkToLocalHref(permalink: string): string {
} }
try { try {
const m = decodeURIComponent(permalink).match(matrixLinkify.ELEMENT_URL_PATTERN); const m = decodeURIComponent(permalink).match(ELEMENT_URL_PATTERN);
if (m) { if (m) {
return m[1]; return m[1];
} }
@ -386,7 +386,7 @@ export function getPrimaryPermalinkEntity(permalink: string): string {
// If not a permalink, try the vector patterns. // If not a permalink, try the vector patterns.
if (!permalinkParts) { if (!permalinkParts) {
const m = permalink.match(matrixLinkify.ELEMENT_URL_PATTERN); const m = permalink.match(ELEMENT_URL_PATTERN);
if (m) { if (m) {
// A bit of a hack, but it gets the job done // A bit of a hack, but it gets the job done
const handler = new ElementPermalinkConstructor("http://localhost"); const handler = new ElementPermalinkConstructor("http://localhost");

View file

@ -13,14 +13,8 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import * as linkifyjs from 'linkifyjs';
import Markdown from "../src/Markdown";
import matrixLinkify from '../src/linkify-matrix';
beforeAll(() => { import Markdown from "../src/Markdown";
// We need to call linkifier plugins before running those tests
matrixLinkify(linkifyjs);
});
describe("Markdown parser test", () => { describe("Markdown parser test", () => {
describe("fixing HTML links", () => { describe("fixing HTML links", () => {

View file

@ -13,12 +13,7 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import * as linkify from "linkifyjs"; import { linkify } from '../src/linkify-matrix';
import linkifyMatrix from '../src/linkify-matrix';
beforeAll(() => {
linkifyMatrix(linkify);
});
describe('linkify-matrix', () => { describe('linkify-matrix', () => {
describe('roomalias', () => { describe('roomalias', () => {
@ -74,7 +69,6 @@ describe('linkify-matrix', () => {
href: "#foo:bar.com", href: "#foo:bar.com",
type: "roomalias", type: "roomalias",
value: "#foo:bar.com", value: "#foo:bar.com",
}])); }]));
}); });
it('accept :NUM (port specifier)', () => { it('accept :NUM (port specifier)', () => {
@ -93,7 +87,6 @@ describe('linkify-matrix', () => {
href: "#foo:bar.com", href: "#foo:bar.com",
type: "roomalias", type: "roomalias",
value: "#foo:bar.com", value: "#foo:bar.com",
}])); }]));
}); });
it('properly parses room alias with dots in name', () => { it('properly parses room alias with dots in name', () => {
@ -103,7 +96,6 @@ describe('linkify-matrix', () => {
href: "#foo.asdf:bar.com", href: "#foo.asdf:bar.com",
type: "roomalias", type: "roomalias",
value: "#foo.asdf:bar.com", value: "#foo.asdf:bar.com",
}])); }]));
}); });
it('does not parse room alias with too many separators', () => { it('does not parse room alias with too many separators', () => {