Skip to content
This repository has been archived by the owner on Sep 11, 2024. It is now read-only.

Upgrade linkify to v3.0 #7282

Merged
merged 11 commits into from
Jan 18, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,9 @@
"is-ip": "^3.1.0",
"jszip": "^3.7.0",
"katex": "^0.12.0",
"linkifyjs": "^2.1.9",
"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",
Expand Down
172 changes: 105 additions & 67 deletions src/linkify-matrix.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,10 @@ limitations under the License.
*/

import * as linkifyjs from 'linkifyjs';
import linkifyElement from 'linkifyjs/element';
import linkifyString from 'linkifyjs/string';
import linkifyElement from 'linkify-element';
import linkifyString from 'linkify-string';
import { RoomMember } from 'matrix-js-sdk/src/models/room-member';
import { registerPlugin } from 'linkifyjs';

import { baseUrl } from "./utils/permalinks/SpecPermalinkConstructor";
import {
Expand All @@ -37,73 +38,82 @@ enum Type {
GroupId = "groupid"
}

// 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,
}
// Linkify stuff doesn't type scanner/parser/utils properly :/
function matrixOpaqueIdLinkifyParser({
scanner,
parser,
utils,
token,
name,
}: {
scanner: any;
parser: any;
utils: any;
token: '#' | '+' | '@';
name: Type;
}) {
const {
DOMAIN,
DOT,
// A generic catchall text token
TEXT,
NUM,
TLD,
COLON,
SYM,
UNDERSCORE,
// because 'localhost' is tokenised to the localhost token,
// usernames @localhost:foo.com are otherwise not matched!
LOCALHOST,
} = scanner.tokens;

/**
* 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
const TT = linkify.scanner.TOKENS;
// Multi tokens
const MT = linkify.parser.TOKENS;
const MultiToken = MT.Base;
const S_START = linkify.parser.start;

const TOKEN = function(value) {
MultiToken.call(this, value);
this.type = type;
this.isLink = true;
};
TOKEN.prototype = new MultiToken();

const S_TOKEN = S_START.jump(token);
const S_TOKEN_NAME = new linkify.parser.State();
const S_TOKEN_NAME_COLON = new linkify.parser.State();
const S_TOKEN_NAME_COLON_DOMAIN = new linkify.parser.State(TOKEN);
const S_TOKEN_NAME_COLON_DOMAIN_DOT = new linkify.parser.State();
const S_MX_LINK = new linkify.parser.State(TOKEN);
const S_MX_LINK_COLON = new linkify.parser.State();
const S_MX_LINK_COLON_NUM = new linkify.parser.State(TOKEN);

const allowedFreeformTokens = [
TT.DOT,
TT.PLUS,
TT.NUM,
TT.DOMAIN,
TT.TLD,
TT.UNDERSCORE,
token,
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!
TT.LOCALHOST,
LOCALHOST,
SYM,
UNDERSCORE,
TEXT,
];
const domainpartTokens = [DOMAIN, NUM, TLD, LOCALHOST];

const INITIAL_STATE = S_START.tt(token);

S_TOKEN.on(allowedFreeformTokens, S_TOKEN_NAME);
S_TOKEN_NAME.on(allowedFreeformTokens, S_TOKEN_NAME);
S_TOKEN_NAME.on(TT.DOMAIN, S_TOKEN_NAME);
const LOCALPART_STATE = INITIAL_STATE.tt(DOMAIN);
for (const token of localpartTokens) {
INITIAL_STATE.tt(token, LOCALPART_STATE);
LOCALPART_STATE.tt(token, LOCALPART_STATE);
}
const LOCALPART_STATE_DOT = LOCALPART_STATE.tt(DOT);
for (const token of localpartTokens) {
LOCALPART_STATE_DOT.tt(token, LOCALPART_STATE);
}

S_TOKEN_NAME.on(TT.COLON, S_TOKEN_NAME_COLON);
const DOMAINPART_STATE_DOT = LOCALPART_STATE.tt(COLON);
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);
// we are done if we have a domain
DOMAINPART_STATE.tt(token, matrixSymbol);
}

S_TOKEN_NAME_COLON.on(TT.DOMAIN, S_TOKEN_NAME_COLON_DOMAIN);
S_TOKEN_NAME_COLON.on(TT.LOCALHOST, S_MX_LINK); // accept #foo:localhost
S_TOKEN_NAME_COLON.on(TT.TLD, S_MX_LINK); // accept #foo:com (mostly for (TLD|DOMAIN)+ mixing)
S_TOKEN_NAME_COLON_DOMAIN.on(TT.DOT, S_TOKEN_NAME_COLON_DOMAIN_DOT);
S_TOKEN_NAME_COLON_DOMAIN_DOT.on(TT.DOMAIN, S_TOKEN_NAME_COLON_DOMAIN);
S_TOKEN_NAME_COLON_DOMAIN_DOT.on(TT.TLD, S_MX_LINK);
// accept repeated TLDs (e.g .org.uk) but do not accept double dots: ..
for (const token of domainpartTokens) {
DOMAINPART_STATE_DOT.tt(token, DOMAINPART_STATE);
}

S_MX_LINK.on(TT.DOT, S_TOKEN_NAME_COLON_DOMAIN_DOT); // accept repeated TLDs (e.g .org.uk)
S_MX_LINK.on(TT.COLON, S_MX_LINK_COLON); // do not accept trailing `:`
S_MX_LINK_COLON.on(TT.NUM, S_MX_LINK_COLON_NUM); // but do accept :NUM (port specifier)
const PORT_STATE = DOMAINPART_STATE.tt(COLON);

PORT_STATE.tt(NUM, matrixSymbol);
}

function onUserClick(event: MouseEvent, userId: string) {
Expand Down Expand Up @@ -199,10 +209,12 @@ export const options = {
}
},

linkAttributes: {
attributes: {
rel: 'noreferrer noopener',
},

className: 'linkified',

target: function(href: string, type: Type | string): string {
if (type === Type.URL) {
try {
Expand All @@ -221,12 +233,38 @@ export const options = {
};

// 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);
registerPlugin(Type.RoomAlias, ({ scanner, parser, utils }) => {
const token = scanner.tokens.POUND as '#';
return matrixOpaqueIdLinkifyParser({
scanner,
parser,
utils,
token,
name: Type.RoomAlias,
});
});

registerPlugin(Type.GroupId, ({ scanner, parser, utils }) => {
const token = scanner.tokens.PLUS as '+';
return matrixOpaqueIdLinkifyParser({
scanner,
parser,
utils,
token,
name: Type.GroupId,
});
});

registerPlugin(Type.UserId, ({ scanner, parser, utils }) => {
const token = scanner.tokens.AT as '@';
return matrixOpaqueIdLinkifyParser({
scanner,
parser,
utils,
token,
name: Type.UserId,
});
});

export const linkify = linkifyjs;
export const _linkifyElement = linkifyElement;
Expand Down
Loading