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:
parent
3b9e39ffca
commit
961fec9081
7 changed files with 99 additions and 206 deletions
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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'];
|
||||||
|
|
||||||
|
|
|
@ -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',
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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");
|
||||||
|
|
|
@ -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", () => {
|
||||||
|
|
|
@ -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', () => {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue