Parse matrix-schemed URIs (#7453)

Co-authored-by: J. Ryan Stinnett <jryans@gmail.com>
Co-authored-by: Dariusz Niemczyk <dariuszn@element.io>
Co-authored-by: Timo K <toger5@hotmail.de>

With this pr all href use matrix matrix.to links. As a consequence right-click copy link will always return get you a sharable matrix.to link.
This commit is contained in:
Travis Ralston 2022-01-20 10:18:47 -07:00 committed by GitHub
parent f59ea6d7ad
commit 6712a5b1c5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 254 additions and 100 deletions

View file

@ -58,6 +58,9 @@
},
"dependencies": {
"@babel/runtime": "^7.12.5",
"@matrix-org/linkify-element": "^4.0.0-rc.5",
"@matrix-org/linkify-string": "^4.0.0-rc.5",
"@matrix-org/linkifyjs": "^4.0.0-rc.5",
"@sentry/browser": "^6.11.0",
"@sentry/tracing": "^6.11.0",
"@types/geojson": "^7946.0.8",
@ -85,9 +88,6 @@
"is-ip": "^3.1.0",
"jszip": "^3.7.0",
"katex": "^0.12.0",
"linkify-element": "^3.0.4",
"linkify-string": "^3.0.4",
"linkifyjs": "^3.0.5",
"lodash": "^4.17.20",
"maplibre-gl": "^1.15.2",
"matrix-analytics-events": "https://github.com/matrix-org/matrix-analytics-events.git#1eab4356548c97722a183912fda1ceabbe8cc7c1",
@ -147,7 +147,6 @@
"@types/file-saver": "^2.0.3",
"@types/flux": "^3.1.9",
"@types/jest": "^26.0.20",
"@types/linkifyjs": "^2.1.3",
"@types/lodash": "^4.14.168",
"@types/modernizr": "^3.5.3",
"@types/node": "^14.14.22",

View file

@ -175,9 +175,10 @@ const transformTags: IExtendedSanitizeOptions["transformTags"] = { // custom to
if (attribs.href) {
attribs.target = '_blank'; // by default
const transformed = tryTransformPermalinkToLocalHref(attribs.href);
if (transformed !== attribs.href || attribs.href.match(ELEMENT_URL_PATTERN)) {
attribs.href = transformed;
const transformed = tryTransformPermalinkToLocalHref(attribs.href); // only used to check if it is a link that can be handled locally
if (transformed !== attribs.href || // it could be converted so handle locally symbols e.g. @user:server.tdl, matrix: and matrix.to
attribs.href.match(ELEMENT_URL_PATTERN) // for https:vector|riot...
) {
delete attribs.target;
}
}

View file

@ -24,7 +24,7 @@ import * as sdk from '../../../index';
import dis from '../../../dispatcher/dispatcher';
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import FlairStore from "../../../stores/FlairStore";
import { getPrimaryPermalinkEntity, parseAppLocalLink } from "../../../utils/permalinks/Permalinks";
import { getPrimaryPermalinkEntity, parsePermalink } from "../../../utils/permalinks/Permalinks";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import { Action } from "../../../dispatcher/actions";
import { mediaFromMxc } from "../../../customisations/Media";
@ -85,7 +85,7 @@ class Pill extends React.Component {
if (nextProps.url) {
if (nextProps.inMessage) {
const parts = parseAppLocalLink(nextProps.url);
const parts = parsePermalink(nextProps.url);
resourceId = parts.primaryEntityId; // The room/user ID
prefix = parts.sigil; // The first character of prefix
} else {

View file

@ -15,13 +15,14 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import * as linkifyjs from 'linkifyjs';
import linkifyElement from 'linkify-element';
import linkifyString from 'linkify-string';
import * as linkifyjs from '@matrix-org/linkifyjs';
import linkifyElement from '@matrix-org/linkify-element';
import linkifyString from '@matrix-org/linkify-string';
import { RoomMember } from 'matrix-js-sdk/src/models/room-member';
import { registerPlugin } from 'linkifyjs';
import { registerCustomProtocol, registerPlugin } from '@matrix-org/linkifyjs';
import { baseUrl } from "./utils/permalinks/SpecPermalinkConstructor";
//linkifyjs/src/core/fsm
import { baseUrl } from "./utils/permalinks/MatrixToPermalinkConstructor";
import {
parsePermalink,
tryTransformEntityToPermalink,
@ -31,7 +32,7 @@ import dis from './dispatcher/dispatcher';
import { Action } from './dispatcher/actions';
import { ViewUserPayload } from './dispatcher/payloads/ViewUserPayload';
enum Type {
export enum Type {
URL = "url",
UserId = "userid",
RoomAlias = "roomalias",
@ -53,41 +54,29 @@ function matrixOpaqueIdLinkifyParser({
name: Type;
}) {
const {
DOMAIN,
DOT,
// A generic catchall text token
TEXT,
// IPV4 necessity
NUM,
TLD,
COLON,
SYM,
HYPHEN,
UNDERSCORE,
// because 'localhost' is tokenised to the localhost token,
// usernames @localhost:foo.com are otherwise not matched!
LOCALHOST,
domain,
} = scanner.tokens;
const S_START = parser.start;
const matrixSymbol = utils.createTokenClass(name, { isLink: true });
const localpartTokens = [
DOMAIN,
// IPV4 necessity
NUM,
TLD,
// because 'localhost' is tokenised to the localhost token,
// usernames @localhost:foo.com are otherwise not matched!
LOCALHOST,
SYM,
UNDERSCORE,
TEXT,
];
const domainpartTokens = [DOMAIN, NUM, TLD, LOCALHOST];
const localpartTokens = [domain, TLD, LOCALHOST, SYM, UNDERSCORE, HYPHEN];
const domainpartTokens = [domain, TLD, LOCALHOST, HYPHEN];
const INITIAL_STATE = S_START.tt(token);
const LOCALPART_STATE = INITIAL_STATE.tt(DOMAIN);
const LOCALPART_STATE = INITIAL_STATE.tt(domain);
for (const token of localpartTokens) {
INITIAL_STATE.tt(token, LOCALPART_STATE);
LOCALPART_STATE.tt(token, LOCALPART_STATE);
@ -98,7 +87,7 @@ function matrixOpaqueIdLinkifyParser({
}
const DOMAINPART_STATE_DOT = LOCALPART_STATE.tt(COLON);
const DOMAINPART_STATE = DOMAINPART_STATE_DOT.tt(DOMAIN);
const DOMAINPART_STATE = DOMAINPART_STATE_DOT.tt(domain);
DOMAINPART_STATE.tt(DOT, DOMAINPART_STATE_DOT);
for (const token of domainpartTokens) {
DOMAINPART_STATE.tt(token, DOMAINPART_STATE);
@ -117,6 +106,7 @@ function matrixOpaqueIdLinkifyParser({
}
function onUserClick(event: MouseEvent, userId: string) {
event.preventDefault();
const member = new RoomMember(null, userId);
if (!member) { return; }
dis.dispatch<ViewUserPayload>({
@ -124,6 +114,7 @@ function onUserClick(event: MouseEvent, userId: string) {
member: member,
});
}
function onAliasClick(event: MouseEvent, roomAlias: string) {
event.preventDefault();
dis.dispatch({
@ -131,6 +122,7 @@ function onAliasClick(event: MouseEvent, roomAlias: string) {
room_alias: roomAlias,
});
}
function onGroupClick(event: MouseEvent, groupId: string) {
event.preventDefault();
dis.dispatch({ action: 'view_group', group_id: groupId });
@ -168,6 +160,19 @@ export const options = {
onUserClick(e, permalink.userId);
},
};
} else {
// for events, rooms etc. (anything other then users)
const localHref = tryTransformPermalinkToLocalHref(href);
if (localHref !== href) {
// it could be converted to a localHref -> therefore handle locally
return {
// @ts-ignore see https://linkify.js.org/docs/options.html
click: function(e) {
e.preventDefault();
window.location.hash = localHref;
},
};
}
}
} catch (e) {
// OK fine, it's not actually a permalink
@ -178,21 +183,24 @@ export const options = {
return {
// @ts-ignore see https://linkify.js.org/docs/options.html
click: function(e) {
onUserClick(e, href);
const userId = parsePermalink(href).userId;
onUserClick(e, userId);
},
};
case Type.RoomAlias:
return {
// @ts-ignore see https://linkify.js.org/docs/options.html
click: function(e) {
onAliasClick(e, href);
const alias = parsePermalink(href).roomIdOrAlias;
onAliasClick(e, alias);
},
};
case Type.GroupId:
return {
// @ts-ignore see https://linkify.js.org/docs/options.html
click: function(e) {
onGroupClick(e, href);
const groupId = parsePermalink(href).groupId;
onGroupClick(e, groupId);
},
};
}
@ -219,7 +227,9 @@ export const options = {
if (type === Type.URL) {
try {
const transformed = tryTransformPermalinkToLocalHref(href);
if (transformed !== href || decodeURIComponent(href).match(ELEMENT_URL_PATTERN)) {
if (transformed !== href || // if it could be converted to handle locally for matrix symbols e.g. @user:server.tdl and matrix.to
decodeURIComponent(href).match(ELEMENT_URL_PATTERN) // for https:vector|riot...
) {
return null;
} else {
return '_blank';
@ -266,6 +276,8 @@ registerPlugin(Type.UserId, ({ scanner, parser, utils }) => {
});
});
registerCustomProtocol("matrix", true);
export const linkify = linkifyjs;
export const _linkifyElement = linkifyElement;
export const _linkifyString = linkifyString;

View file

@ -0,0 +1,105 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import PermalinkConstructor, { PermalinkParts } from "./PermalinkConstructor";
/**
* Generates matrix: scheme permalinks
*/
export default class MatrixSchemePermalinkConstructor extends PermalinkConstructor {
constructor() {
super();
}
private encodeEntity(entity: string): string {
if (entity[0] === "!") {
return `roomid/${entity.slice(1)}`;
} else if (entity[0] === "#") {
return `r/${entity.slice(1)}`;
} else if (entity[0] === "@") {
return `u/${entity.slice(1)}`;
} else if (entity[0] === "$") {
return `e/${entity.slice(1)}`;
}
throw new Error("Cannot encode entity: " + entity);
}
forEvent(roomId: string, eventId: string, serverCandidates: string[]): string {
return `matrix:${this.encodeEntity(roomId)}` +
`/${this.encodeEntity(eventId)}${this.encodeServerCandidates(serverCandidates)}`;
}
forRoom(roomIdOrAlias: string, serverCandidates: string[]): string {
return `matrix:${this.encodeEntity(roomIdOrAlias)}${this.encodeServerCandidates(serverCandidates)}`;
}
forUser(userId: string): string {
return `matrix:${this.encodeEntity(userId)}`;
}
forGroup(groupId: string): string {
throw new Error("Deliberately not implemented");
}
forEntity(entityId: string): string {
return `matrix:${this.encodeEntity(entityId)}`;
}
isPermalinkHost(testHost: string): boolean {
// TODO: Change API signature to accept the URL for checking
return testHost === "";
}
encodeServerCandidates(candidates: string[]) {
if (!candidates || candidates.length === 0) return '';
return `?via=${candidates.map(c => encodeURIComponent(c)).join("&via=")}`;
}
parsePermalink(fullUrl: string): PermalinkParts {
if (!fullUrl || !fullUrl.startsWith("matrix:")) {
throw new Error("Does not appear to be a permalink");
}
const parts = fullUrl.substring("matrix:".length).split('/');
const identifier = parts[0];
const entityNoSigil = parts[1];
if (identifier === 'u') {
// Probably a user, no further parsing needed.
return PermalinkParts.forUser(`@${entityNoSigil}`);
} else if (identifier === 'r' || identifier === 'roomid') {
const sigil = identifier === 'r' ? '#' : '!';
if (parts.length === 2) { // room without event permalink
const [roomId, query = ""] = entityNoSigil.split("?");
const via = query.split(/&?via=/g).filter(p => !!p);
return PermalinkParts.forRoom(`${sigil}${roomId}`, via);
}
if (parts[2] === 'e') { // event permalink
const eventIdAndQuery = parts.length > 3 ? parts.slice(3).join('/') : "";
const [eventId, query = ""] = eventIdAndQuery.split("?");
const via = query.split(/&?via=/g).filter(p => !!p);
return PermalinkParts.forEvent(`${sigil}${entityNoSigil}`, `$${eventId}`, via);
}
throw new Error("Faulty room permalink");
} else {
throw new Error("Unknown entity type in permalink");
}
}
}

View file

@ -22,7 +22,7 @@ export const baseUrl = `https://${host}`;
/**
* Generates matrix.to permalinks
*/
export default class SpecPermalinkConstructor extends PermalinkConstructor {
export default class MatrixToPermalinkConstructor extends PermalinkConstructor {
constructor() {
super();
}

View file

@ -23,11 +23,12 @@ import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import { logger } from "matrix-js-sdk/src/logger";
import { MatrixClientPeg } from "../../MatrixClientPeg";
import SpecPermalinkConstructor, { baseUrl as matrixtoBaseUrl } from "./SpecPermalinkConstructor";
import MatrixToPermalinkConstructor, { baseUrl as matrixtoBaseUrl } from "./MatrixToPermalinkConstructor";
import PermalinkConstructor, { PermalinkParts } from "./PermalinkConstructor";
import ElementPermalinkConstructor from "./ElementPermalinkConstructor";
import SdkConfig from "../../SdkConfig";
import { ELEMENT_URL_PATTERN } from "../../linkify-matrix";
import MatrixSchemePermalinkConstructor from "./MatrixSchemePermalinkConstructor";
// The maximum number of servers to pick when working out which servers
// to add to permalinks. The servers are appended as ?via=example.org
@ -312,14 +313,14 @@ export function makeGroupPermalink(groupId: string): string {
export function isPermalinkHost(host: string): boolean {
// Always check if the permalink is a spec permalink (callers are likely to call
// parsePermalink after this function).
if (new SpecPermalinkConstructor().isPermalinkHost(host)) return true;
if (new MatrixToPermalinkConstructor().isPermalinkHost(host)) return true;
return getPermalinkConstructor().isPermalinkHost(host);
}
/**
* Transforms an entity (permalink, room alias, user ID, etc) into a local URL
* if possible. If the given entity is not found to be valid enough to be converted
* then a null value will be returned.
* if possible. If it is already a permalink (matrix.to) it gets returned
* unchanged.
* @param {string} entity The entity to transform.
* @returns {string|null} The transformed permalink or null if unable.
*/
@ -327,12 +328,31 @@ export function tryTransformEntityToPermalink(entity: string): string {
if (!entity) return null;
// Check to see if it is a bare entity for starters
if (entity[0] === '#' || entity[0] === '!') return makeRoomPermalink(entity);
{if (entity[0] === '#' || entity[0] === '!') return makeRoomPermalink(entity);}
if (entity[0] === '@') return makeUserPermalink(entity);
if (entity[0] === '+') return makeGroupPermalink(entity);
// Then try and merge it into a permalink
return tryTransformPermalinkToLocalHref(entity);
if (entity.slice(0, 7) === "matrix:") {
try {
const permalinkParts = parsePermalink(entity);
if (permalinkParts) {
if (permalinkParts.roomIdOrAlias) {
const eventIdPart = permalinkParts.eventId ? `/${permalinkParts.eventId}` : '';
let pl = matrixtoBaseUrl+`/#/${permalinkParts.roomIdOrAlias}${eventIdPart}`;
if (permalinkParts.viaServers.length > 0) {
pl += new MatrixToPermalinkConstructor().encodeServerCandidates(permalinkParts.viaServers);
}
return pl;
} else if (permalinkParts.groupId) {
return matrixtoBaseUrl + `/#/${permalinkParts.groupId}`;
} else if (permalinkParts.userId) {
return matrixtoBaseUrl + `/#/${permalinkParts.userId}`;
}
}
} catch {}
}
return entity;
}
/**
@ -342,7 +362,7 @@ export function tryTransformEntityToPermalink(entity: string): string {
* @returns {string} The transformed permalink or original URL if unable.
*/
export function tryTransformPermalinkToLocalHref(permalink: string): string {
if (!permalink.startsWith("http:") && !permalink.startsWith("https:")) {
if (!permalink.startsWith("http:") && !permalink.startsWith("https:") && !permalink.startsWith("matrix:")) {
return permalink;
}
@ -364,7 +384,7 @@ export function tryTransformPermalinkToLocalHref(permalink: string): string {
const eventIdPart = permalinkParts.eventId ? `/${permalinkParts.eventId}` : '';
permalink = `#/room/${permalinkParts.roomIdOrAlias}${eventIdPart}`;
if (permalinkParts.viaServers.length > 0) {
permalink += new SpecPermalinkConstructor().encodeServerCandidates(permalinkParts.viaServers);
permalink += new MatrixToPermalinkConstructor().encodeServerCandidates(permalinkParts.viaServers);
}
} else if (permalinkParts.groupId) {
permalink = `#/group/${permalinkParts.groupId}`;
@ -411,13 +431,15 @@ function getPermalinkConstructor(): PermalinkConstructor {
return new ElementPermalinkConstructor(elementPrefix);
}
return new SpecPermalinkConstructor();
return new MatrixToPermalinkConstructor();
}
export function parsePermalink(fullUrl: string): PermalinkParts {
const elementPrefix = SdkConfig.get()['permalinkPrefix'];
if (decodeURIComponent(fullUrl).startsWith(matrixtoBaseUrl)) {
return new SpecPermalinkConstructor().parsePermalink(decodeURIComponent(fullUrl));
return new MatrixToPermalinkConstructor().parsePermalink(decodeURIComponent(fullUrl));
} else if (fullUrl.startsWith("matrix:")) {
return new MatrixSchemePermalinkConstructor().parsePermalink(fullUrl);
} else if (elementPrefix && fullUrl.startsWith(elementPrefix)) {
return new ElementPermalinkConstructor(elementPrefix).parsePermalink(fullUrl);
}
@ -425,23 +447,6 @@ export function parsePermalink(fullUrl: string): PermalinkParts {
return null; // not a permalink we can handle
}
/**
* Parses an app local link (`#/(user|room|group)/identifer`) to a Matrix entity
* (room, user, group). Such links are produced by `HtmlUtils` when encountering
* links, which calls `tryTransformPermalinkToLocalHref` in this module.
* @param {string} localLink The app local link
* @returns {PermalinkParts}
*/
export function parseAppLocalLink(localLink: string): PermalinkParts {
try {
const segments = localLink.replace("#/", "");
return ElementPermalinkConstructor.parseAppRoute(segments);
} catch (e) {
// Ignore failures
}
return null;
}
function getServerName(userId: string): string {
return userId.split(":").splice(1).join(":");
}

View file

@ -22,7 +22,7 @@ import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { MatrixClientPeg } from '../MatrixClientPeg';
import SettingsStore from "../settings/SettingsStore";
import Pill from "../components/views/elements/Pill";
import { parseAppLocalLink } from "./permalinks/Permalinks";
import { parsePermalink } from "./permalinks/Permalinks";
/**
* Recurses depth-first through a DOM tree, converting matrix.to links
@ -46,7 +46,7 @@ export function pillifyLinks(nodes: ArrayLike<Element>, mxEvent: MatrixEvent, pi
if (node.tagName === "A" && node.getAttribute("href")) {
const href = node.getAttribute("href");
const parts = parseAppLocalLink(href);
const parts = parsePermalink(href);
// If the link is a (localised) matrix.to link, replace it with a pill
// We don't want to pill event permalinks, so those are ignored.
if (parts && !parts.eventId) {

View file

@ -245,7 +245,7 @@ describe("<TextualBody />", () => {
const content = wrapper.find(".mx_EventTile_body");
expect(content.html()).toBe(
'<span class="mx_EventTile_body markdown-body" dir="auto">' +
'An <a href="#/room/!ZxbRYPQXDXKGmDnJNg:example.com/' +
'An <a href="https://matrix.to/#/!ZxbRYPQXDXKGmDnJNg:example.com/' +
'$16085560162aNpaH:example.com?via=example.com" ' +
'rel="noreferrer noopener">event link</a> with text</span>',
);
@ -274,7 +274,8 @@ describe("<TextualBody />", () => {
const content = wrapper.find(".mx_EventTile_body");
expect(content.html()).toBe(
'<span class="mx_EventTile_body markdown-body" dir="auto">' +
'A <span><a class="mx_Pill mx_RoomPill" href="#/room/!ZxbRYPQXDXKGmDnJNg:example.com' +
'A <span><a class="mx_Pill mx_RoomPill" ' +
'href="https://matrix.to/#/!ZxbRYPQXDXKGmDnJNg:example.com' +
'?via=example.com&amp;via=bob.com"' +
'><img class="mx_BaseAvatar mx_BaseAvatar_image" ' +
'src="mxc://avatar.url/room.png" ' +

View file

@ -33,7 +33,7 @@ import { createPartCreator, createRenderer } from "../../../editor/mock";
import { createTestClient, mkEvent, mkStubRoom } from "../../../test-utils";
import BasicMessageComposer from "../../../../src/components/views/rooms/BasicMessageComposer";
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
import SpecPermalinkConstructor from "../../../../src/utils/permalinks/SpecPermalinkConstructor";
import MatrixToPermalinkConstructor from "../../../../src/utils/permalinks/MatrixToPermalinkConstructor";
import defaultDispatcher from "../../../../src/dispatcher/dispatcher";
import DocumentOffset from '../../../../src/editor/offset';
import { Layout } from '../../../../src/settings/enums/Layout';
@ -166,7 +166,7 @@ describe('<SendMessageComposer/>', () => {
<SendMessageComposer
room={mockRoom as any}
placeholder="placeholder string"
permalinkCreator={new SpecPermalinkConstructor() as any}
permalinkCreator={new MatrixToPermalinkConstructor() as any}
/>
</RoomContext.Provider>
</MatrixClientContext.Provider>);
@ -188,7 +188,7 @@ describe('<SendMessageComposer/>', () => {
<SendMessageComposer
room={mockRoom as any}
placeholder=""
permalinkCreator={new SpecPermalinkConstructor() as any}
permalinkCreator={new MatrixToPermalinkConstructor() as any}
replyToEvent={mockEvent}
/>
</RoomContext.Provider>
@ -234,7 +234,7 @@ describe('<SendMessageComposer/>', () => {
<SendMessageComposer
room={mockRoom as any}
placeholder=""
permalinkCreator={new SpecPermalinkConstructor() as any}
permalinkCreator={new MatrixToPermalinkConstructor() as any}
/>
</RoomContext.Provider>
</MatrixClientContext.Provider>);
@ -263,7 +263,7 @@ describe('<SendMessageComposer/>', () => {
<SendMessageComposer
room={mockRoom as any}
placeholder="placeholder"
permalinkCreator={new SpecPermalinkConstructor() as any}
permalinkCreator={new MatrixToPermalinkConstructor() as any}
replyToEvent={mockEvent}
/>
</RoomContext.Provider>
@ -297,7 +297,7 @@ describe('<SendMessageComposer/>', () => {
<SendMessageComposer
room={mockRoom as any}
placeholder=""
permalinkCreator={new SpecPermalinkConstructor() as any}
permalinkCreator={new MatrixToPermalinkConstructor() as any}
relation={{
rel_type: RelationType.Thread,
event_id: "myFakeThreadId",

View file

@ -13,7 +13,7 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { linkify } from '../src/linkify-matrix';
import { linkify, Type } from '../src/linkify-matrix';
describe('linkify-matrix', () => {
const linkTypesByInitialCharacter = {
@ -177,6 +177,18 @@ describe('linkify-matrix', () => {
isLink: true,
}]));
});
it('accept hyphens in name ' + char + 'foo-bar:server.com', () => {
const test = '' + char + 'foo-bar:server.com';
const found = linkify.find(test);
expect(found).toEqual(([{
href: char + "foo-bar:server.com",
type,
value: char + "foo-bar:server.com",
start: 0,
end: test.length,
isLink: true,
}]));
});
it('ignores trailing `:`', () => {
const test = '' + char + 'foo:bar.com:';
const found = linkify.find(test);
@ -264,4 +276,30 @@ describe('linkify-matrix', () => {
describe('userid plugin', () => {
genTests('@');
});
describe('matrix uri', () => {
const AcceptedMatrixUris = [
'matrix:u/foo_bar:server.uk',
'matrix:r/foo-bar:server.uk',
'matrix:roomid/somewhere:example.org?via=elsewhere.ca',
'matrix:r/somewhere:example.org',
'matrix:r/somewhere:example.org/e/event',
'matrix:roomid/somewhere:example.org/e/event?via=elsewhere.ca',
'matrix:u/alice:example.org?action=chat',
];
for (const matrixUri of AcceptedMatrixUris) {
it('accepts ' + matrixUri, () => {
const test = matrixUri;
const found = linkify.find(test);
expect(found).toEqual(([{
href: matrixUri,
type: Type.URL,
value: matrixUri,
end: matrixUri.length,
start: 0,
isLink: true,
}]));
});
}
});
});

View file

@ -1384,6 +1384,21 @@
resolved "https://registry.yarnpkg.com/@mapbox/whoots-js/-/whoots-js-3.1.0.tgz#497c67a1cef50d1a2459ba60f315e448d2ad87fe"
integrity sha512-Es6WcD0nO5l+2BOQS4uLfNPYQaNDfbot3X1XUoloz+x0mPDS3eeORZJl06HXjwBG1fOGwCRnzK88LMdxKRrd6Q==
"@matrix-org/linkify-element@^4.0.0-rc.5":
version "4.0.0-rc.5"
resolved "https://registry.yarnpkg.com/@matrix-org/linkify-element/-/linkify-element-4.0.0-rc.5.tgz#0c27e81272638674ba4162fec2fd131b2c36c163"
integrity sha512-6sdJ5x9EZpUNVZqc2Dig8Q4hySdL2fdE/ivehk4L3y3rV7tMD0fRs5rXQQ4wcgCMhGAAGbscbNiS8pyZIy9Hlg==
"@matrix-org/linkify-string@^4.0.0-rc.5":
version "4.0.0-rc.5"
resolved "https://registry.yarnpkg.com/@matrix-org/linkify-string/-/linkify-string-4.0.0-rc.5.tgz#139ba23c70a4f5b531656365a6109c122177b132"
integrity sha512-WFyu6+kVEPJsDwZwgCSrtUDeIMDdWIFzRRq5z+MLYHiO3J8G19jvRjRnNm4dwjDUqROWhvWS9b8JG7rbuwjkLQ==
"@matrix-org/linkifyjs@^4.0.0-rc.5":
version "4.0.0-rc.5"
resolved "https://registry.yarnpkg.com/@matrix-org/linkifyjs/-/linkifyjs-4.0.0-rc.5.tgz#3a2885754a8de51164a30e6e09909173e348d6bb"
integrity sha512-HGmEZuUzCOzdsUFM5dQK2R2KhBFnxRfye5CYJhM2EpRTO4t5aTcR6Ey09HuJ/DZevQ9GTFUjkuKAKurQhnAfOA==
"@matrix-org/olm@https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.8.tgz":
version "3.2.8"
resolved "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.8.tgz#8d53636d045e1776e2a2ec6613e57330dd9ce856"
@ -1815,13 +1830,6 @@
resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4=
"@types/linkifyjs@^2.1.3":
version "2.1.4"
resolved "https://registry.yarnpkg.com/@types/linkifyjs/-/linkifyjs-2.1.4.tgz#6b14e35d8d211f2666f602dcabcdc6859617516f"
integrity sha512-UuF0hyWNnLTT4xNJdrQx6OWYMNlPRBtt3fKCaROIx48boQyXkQ4YDDwTEQNi9mlsRX0Hpc6AnFKkDZ6IXkxD4g==
dependencies:
"@types/react" "*"
"@types/lodash@^4.14.168":
version "4.14.178"
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.178.tgz#341f6d2247db528d4a13ddbb374bcdc80406f4f8"
@ -5968,21 +5976,6 @@ lines-and-columns@^1.1.6:
resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632"
integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==
linkify-element@^3.0.4:
version "3.0.4"
resolved "https://registry.yarnpkg.com/linkify-element/-/linkify-element-3.0.4.tgz#3566a3b48d4c211a684f42a23a9964bf53f3a31a"
integrity sha512-xrj2Upv4/XUxvvczoDwojEnzKnfJCHlorAxYmdFPSGNwLz2sXYkYyB7Lw1flkGS7L0yS0dq/HwOotG0Kpaiaxw==
linkify-string@^3.0.4:
version "3.0.4"
resolved "https://registry.yarnpkg.com/linkify-string/-/linkify-string-3.0.4.tgz#6abf1a5e436e800c729274ae08f5703484647f84"
integrity sha512-OnNqqRjlYXaXipIAbBC8sDXsSumI1ftatzFg141Pw9HEXWjTVLFcMZoKbFupshqWRavtNJ6QHLa+u6AlxxgeRw==
linkifyjs@^3.0.5:
version "3.0.5"
resolved "https://registry.yarnpkg.com/linkifyjs/-/linkifyjs-3.0.5.tgz#99e51a3a0c0e232fcb63ebb89eea3ff923378f34"
integrity sha512-1Y9XQH65eQKA9p2xtk+zxvnTeQBG7rdAXSkUG97DmuI/Xhji9uaUzaWxRj6rf9YC0v8KKHkxav7tnLX82Sz5Fg==
loader-utils@^2.0.0:
version "2.0.2"
resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-2.0.2.tgz#d6e3b4fb81870721ae4e0868ab11dd638368c129"