Upgrade linkify to v3.0 (#7282)

Co-authored-by: Timo K <toger5@hotmail.de>
This commit is contained in:
Dariusz Niemczyk 2022-01-18 18:24:16 +01:00 committed by GitHub
parent c0681333bf
commit 336e1ae3b6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 311 additions and 224 deletions

View file

@ -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",

View file

@ -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 {
@ -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 S_START = parser.start;
const matrixSymbol = utils.createTokenClass(name, { isLink: true });
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 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];
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 INITIAL_STATE = S_START.tt(token);
S_TOKEN_NAME.on(TT.COLON, S_TOKEN_NAME_COLON);
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_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);
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_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)
// 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);
}
const PORT_STATE = DOMAINPART_STATE.tt(COLON);
PORT_STATE.tt(NUM, matrixSymbol);
}
function onUserClick(event: MouseEvent, userId: string) {
@ -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 {
@ -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;

View file

@ -16,215 +16,252 @@ limitations under the License.
import { linkify } from '../src/linkify-matrix';
describe('linkify-matrix', () => {
describe('roomalias', () => {
it('properly parses #_foonetic_xkcd:matrix.org', () => {
const test = '#_foonetic_xkcd:matrix.org';
const linkTypesByInitialCharacter = {
'#': 'roomalias',
'@': 'userid',
'+': 'groupid',
};
/**
*
* @param testName Due to all the tests using the same logic underneath, it makes to generate it in a bit smarter way
* @param char
*/
function genTests(char: '#' | '@' | '+') {
const type = linkTypesByInitialCharacter[char];
it('should not parse ' + char + 'foo without domain', () => {
const test = char + "foo";
const found = linkify.find(test);
expect(found).toEqual(([]));
});
describe('ip v4 tests', () => {
it('should properly parse IPs v4 as the domain name', () => {
const test = char + 'potato:1.2.3.4';
const found = linkify.find(test);
expect(found).toEqual(([{
href: char + 'potato:1.2.3.4',
type,
isLink: true,
start: 0,
end: test.length,
value: char + 'potato:1.2.3.4',
}]));
});
it('should properly parse IPs v4 with port as the domain name with attached', () => {
const test = char + 'potato:1.2.3.4:1337';
const found = linkify.find(test);
expect(found).toEqual(([{
href: char + 'potato:1.2.3.4:1337',
type,
isLink: true,
start: 0,
end: test.length,
value: char + 'potato:1.2.3.4:1337',
}]));
});
it('should properly parse IPs v4 as the domain name while ignoring missing port', () => {
const test = char + 'potato:1.2.3.4:';
const found = linkify.find(test);
expect(found).toEqual(([{
href: char + 'potato:1.2.3.4',
type,
isLink: true,
start: 0,
end: test.length - 1,
value: char + 'potato:1.2.3.4',
}]));
});
});
// Currently those tests are failing, as there's missing implementation.
describe.skip('ip v6 tests', () => {
it('should properly parse IPs v6 as the domain name', () => {
const test = char + "username:[1234:5678::abcd]";
const found = linkify.find(test);
expect(found).toEqual([{
href: char + 'username:[1234:5678::abcd]',
type,
isLink: true,
start: 0,
end: test.length,
value: char + 'username:[1234:5678::abcd]',
},
]);
});
it('should properly parse IPs v6 with port as the domain name', () => {
const test = char + "username:[1234:5678::abcd]:1337";
const found = linkify.find(test);
expect(found).toEqual([{
href: char + 'username:[1234:5678::abcd]:1337',
type,
isLink: true,
start: 0,
end: test.length,
value: char + 'username:[1234:5678::abcd]:1337',
},
]);
});
// eslint-disable-next-line max-len
it('should properly parse IPs v6 while ignoring dangling comma when without port name as the domain name', () => {
const test = char + "username:[1234:5678::abcd]:";
const found = linkify.find(test);
expect(found).toEqual([{
href: char + 'username:[1234:5678::abcd]:',
type,
isLink: true,
start: 0,
end: test.length - 1,
value: char + 'username:[1234:5678::abcd]:',
},
]);
});
});
it('properly parses ' + char + '_foonetic_xkcd:matrix.org', () => {
const test = '' + char + '_foonetic_xkcd:matrix.org';
const found = linkify.find(test);
expect(found).toEqual(([{
href: "#_foonetic_xkcd:matrix.org",
type: "roomalias",
value: "#_foonetic_xkcd:matrix.org",
href: char + "_foonetic_xkcd:matrix.org",
type,
value: char + "_foonetic_xkcd:matrix.org",
start: 0,
end: test.length,
isLink: true,
}]));
});
it('properly parses #foo:localhost', () => {
const test = "#foo:localhost";
it('properly parses ' + char + 'foo:localhost', () => {
const test = char + "foo:localhost";
const found = linkify.find(test);
expect(found).toEqual(([{
href: "#foo:localhost",
type: "roomalias",
value: "#foo:localhost",
href: char + "foo:localhost",
type,
value: char + "foo:localhost",
start: 0,
end: test.length,
isLink: true,
}]));
});
it('accept #foo:bar.com', () => {
const test = '#foo:bar.com';
it('accept ' + char + 'foo:bar.com', () => {
const test = '' + char + 'foo:bar.com';
const found = linkify.find(test);
expect(found).toEqual(([{
href: "#foo:bar.com",
type: "roomalias",
value: "#foo:bar.com",
href: char + "foo:bar.com",
type,
value: char + "foo:bar.com",
start: 0,
end: test.length,
isLink: true,
}]));
});
it('accept #foo:com (mostly for (TLD|DOMAIN)+ mixing)', () => {
const test = '#foo:com';
it('accept ' + char + 'foo:com (mostly for (TLD|DOMAIN)+ mixing)', () => {
const test = '' + char + 'foo:com';
const found = linkify.find(test);
expect(found).toEqual(([{
href: "#foo:com",
type: "roomalias",
value: "#foo:com",
href: char + "foo:com",
type,
value: char + "foo:com",
start: 0,
end: test.length,
isLink: true,
}]));
});
it('accept repeated TLDs (e.g .org.uk)', () => {
const test = '#foo:bar.org.uk';
const test = '' + char + 'foo:bar.org.uk';
const found = linkify.find(test);
expect(found).toEqual(([{
href: "#foo:bar.org.uk",
type: "roomalias",
value: "#foo:bar.org.uk",
href: char + "foo:bar.org.uk",
type,
value: char + "foo:bar.org.uk",
start: 0,
end: test.length,
isLink: true,
}]));
});
it('ignores trailing `:`', () => {
const test = '#foo:bar.com:';
const test = '' + char + 'foo:bar.com:';
const found = linkify.find(test);
expect(found).toEqual(([{
href: "#foo:bar.com",
type: "roomalias",
value: "#foo:bar.com",
type,
value: char + "foo:bar.com",
href: char + 'foo:bar.com',
start: 0,
end: test.length - ":".length,
isLink: true,
}]));
});
it('accept :NUM (port specifier)', () => {
const test = '#foo:bar.com:2225';
const test = '' + char + 'foo:bar.com:2225';
const found = linkify.find(test);
expect(found).toEqual(([{
href: "#foo:bar.com:2225",
type: "roomalias",
value: "#foo:bar.com:2225",
href: char + "foo:bar.com:2225",
type,
value: char + "foo:bar.com:2225",
start: 0,
end: test.length,
isLink: true,
}]));
});
it('ignores all the trailing :', () => {
const test = '#foo:bar.com::::';
const test = '' + char + 'foo:bar.com::::';
const found = linkify.find(test);
expect(found).toEqual(([{
href: "#foo:bar.com",
type: "roomalias",
value: "#foo:bar.com",
href: char + "foo:bar.com",
type,
value: char + "foo:bar.com",
end: test.length - 4,
start: 0,
isLink: true,
}]));
});
it('properly parses room alias with dots in name', () => {
const test = '#foo.asdf:bar.com::::';
const test = '' + char + 'foo.asdf:bar.com::::';
const found = linkify.find(test);
expect(found).toEqual(([{
href: "#foo.asdf:bar.com",
type: "roomalias",
value: "#foo.asdf:bar.com",
href: char + "foo.asdf:bar.com",
type,
value: char + "foo.asdf:bar.com",
start: 0,
end: test.length - ":".repeat(4).length,
isLink: true,
}]));
});
it('does not parse room alias with too many separators', () => {
const test = '#foo:::bar.com';
const test = '' + char + 'foo:::bar.com';
const found = linkify.find(test);
expect(found).toEqual(([{
href: "http://bar.com",
type: "url",
value: "bar.com",
isLink: true,
start: 7,
end: test.length,
}]));
});
it('does not parse multiple room aliases in one string', () => {
const test = '#foo:bar.com-baz.com';
const test = '' + char + 'foo:bar.com-baz.com';
const found = linkify.find(test);
expect(found).toEqual(([{
"href": "#foo:bar.com-baz.com",
"type": "roomalias",
"value": "#foo:bar.com-baz.com",
href: char + "foo:bar.com-baz.com",
type,
value: char + "foo:bar.com-baz.com",
end: 20,
start: 0,
isLink: true,
}]));
});
}
describe('roomalias plugin', () => {
genTests('#');
});
describe('groupid', () => {
it('properly parses +foo:localhost', () => {
const test = "+foo:localhost";
const found = linkify.find(test);
expect(found).toEqual(([{
href: "+foo:localhost",
type: "groupid",
value: "+foo:localhost",
}]));
});
it('accept +foo:bar.com', () => {
const test = '+foo:bar.com';
const found = linkify.find(test);
expect(found).toEqual(([{
href: "+foo:bar.com",
type: "groupid",
value: "+foo:bar.com",
}]));
});
it('accept +foo:com (mostly for (TLD|DOMAIN)+ mixing)', () => {
const test = '+foo:com';
const found = linkify.find(test);
expect(found).toEqual(([{
href: "+foo:com",
type: "groupid",
value: "+foo:com",
}]));
});
it('accept repeated TLDs (e.g .org.uk)', () => {
const test = '+foo:bar.org.uk';
const found = linkify.find(test);
expect(found).toEqual(([{
href: "+foo:bar.org.uk",
type: "groupid",
value: "+foo:bar.org.uk",
}]));
});
it('ignore trailing `:`', () => {
const test = '+foo:bar.com:';
const found = linkify.find(test);
expect(found).toEqual(([{
"href": "+foo:bar.com",
"type": "groupid",
"value": "+foo:bar.com",
}]));
});
it('accept :NUM (port specifier)', () => {
const test = '+foo:bar.com:2225';
const found = linkify.find(test);
expect(found).toEqual(([{
href: "+foo:bar.com:2225",
type: "groupid",
value: "+foo:bar.com:2225",
}]));
});
describe('groupid plugin', () => {
genTests('+');
});
describe('userid', () => {
it('should not parse @foo without domain', () => {
const test = "@foo";
const found = linkify.find(test);
expect(found).toEqual(([]));
});
it('accept @foo:bar.com', () => {
const test = '@foo:bar.com';
const found = linkify.find(test);
expect(found).toEqual(([{
href: "@foo:bar.com",
type: "userid",
value: "@foo:bar.com",
}]));
});
it('accept @foo:com (mostly for (TLD|DOMAIN)+ mixing)', () => {
const test = '@foo:com';
const found = linkify.find(test);
expect(found).toEqual(([{
href: "@foo:com",
type: "userid",
value: "@foo:com",
}]));
});
it('accept repeated TLDs (e.g .org.uk)', () => {
const test = '@foo:bar.org.uk';
const found = linkify.find(test);
expect(found).toEqual(([{
href: "@foo:bar.org.uk",
type: "userid",
value: "@foo:bar.org.uk",
}]));
});
it('do not accept trailing `:`', () => {
const test = '@foo:bar.com:';
const found = linkify.find(test);
expect(found).toEqual(([{
href: "@foo:bar.com",
type: "userid",
value: "@foo:bar.com",
}]));
});
it('accept :NUM (port specifier)', () => {
const test = '@foo:bar.com:2225';
const found = linkify.find(test);
expect(found).toEqual(([{
href: "@foo:bar.com:2225",
type: "userid",
value: "@foo:bar.com:2225",
}]));
});
describe('userid plugin', () => {
genTests('@');
});
});

View file

@ -3706,7 +3706,7 @@ eslint-module-utils@^2.7.2:
debug "^3.2.7"
find-up "^2.1.0"
eslint-plugin-import@^2.25.2, eslint-plugin-import@^2.25.4:
eslint-plugin-import@^2.25.4:
version "2.25.4"
resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.25.4.tgz#322f3f916a4e9e991ac7af32032c25ce313209f1"
integrity sha512-/KJBASVFxpu0xg1kIBn9AUa8hQVnszpwgE7Ld0lKAlx7Ie87yzEzCgSkekt+le/YVhiaosO4Y14GDAOc41nfxA==
@ -5968,10 +5968,20 @@ 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==
linkifyjs@^2.1.9:
version "2.1.9"
resolved "https://registry.yarnpkg.com/linkifyjs/-/linkifyjs-2.1.9.tgz#af06e45a2866ff06c4766582590d098a4d584702"
integrity sha512-74ivurkK6WHvHFozVaGtQWV38FzBwSTGNmJolEgFp7QgR2bl6ArUWlvT4GcHKbPe1z3nWYi+VUdDZk16zDOVug==
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"