From 6325ea8328d8fcc5a93baacd3b96fb6089b9dcf2 Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Thu, 11 Jun 2020 01:53:49 +0100 Subject: [PATCH 1/7] Create link parser and formatter --- src/parser/parser.test.ts | 82 ++++++++++++++++++ src/parser/parser.ts | 175 ++++++++++++++++++++++++++++++++++++++ src/parser/types.ts | 55 ++++++++++++ 3 files changed, 312 insertions(+) create mode 100644 src/parser/parser.test.ts create mode 100644 src/parser/parser.ts create mode 100644 src/parser/types.ts diff --git a/src/parser/parser.test.ts b/src/parser/parser.test.ts new file mode 100644 index 0000000..44b3314 --- /dev/null +++ b/src/parser/parser.test.ts @@ -0,0 +1,82 @@ +import { + parseLink, + parsePermalink, + parseArgs, + verifiers, + discriminate, + toURI, +} from "./parser"; +import { LinkDiscriminator } from "./types"; + +const curriedDiscriminate = (id: string) => + discriminate(id, verifiers, LinkDiscriminator.ParseFailed); + +it("types identifiers correctly", () => { + expect(curriedDiscriminate("@user:matrix.org")).toEqual( + LinkDiscriminator.UserId + ); + expect(curriedDiscriminate("!room:matrix.org")).toEqual( + LinkDiscriminator.RoomId + ); + expect( + curriedDiscriminate("!somewhere:example.org/$event:example.org") + ).toEqual(LinkDiscriminator.Permalink); + expect(curriedDiscriminate("+group:matrix.org")).toEqual( + LinkDiscriminator.GroupId + ); + expect(curriedDiscriminate("#alias:matrix.org")).toEqual( + LinkDiscriminator.Alias + ); +}); + +it("types garbadge as such", () => { + expect(curriedDiscriminate("sdfa;fdlkja")).toEqual( + LinkDiscriminator.ParseFailed + ); + expect(curriedDiscriminate("$event$matrix.org")).toEqual( + LinkDiscriminator.ParseFailed + ); + expect(curriedDiscriminate("/user:matrix.org")).toEqual( + LinkDiscriminator.ParseFailed + ); +}); + +it("parses vias", () => { + expect( + parseArgs("via=example.org&via=alt.example.org") + ).toHaveProperty("vias", ["example.org", "alt.example.org"]); +}); + +it("parses sharer", () => { + expect(parseArgs("sharer=blah")).toHaveProperty("sharer", "blah"); +}); + +it("parses random args", () => { + expect(parseArgs("via=qreqrqwer&banter=2342")).toHaveProperty( + "extras.banter", + ["2342"] + ); +}); + +it("parses permalinks", () => { + expect(parsePermalink("!somewhere:example.org/$event:example.org")).toEqual({ + roomKind: LinkDiscriminator.RoomId, + roomLink: "!somewhere:example.org", + eventId: "$event:example.org", + }); +}); + +it("formats links correctly", () => { + const bigLink = + "!somewhere:example.org/$event:example.org?via=dfasdf&via=jfjafjaf&uselesstag=useless"; + const host = "matrix.org"; + const prefix = host + "/#/"; + const parse = parseLink(bigLink); + + switch (parse.kind) { + case LinkDiscriminator.ParseFailed: + fail("Parse failed"); + default: + expect(toURI(host, parse)).toEqual(prefix + bigLink); + } +}); diff --git a/src/parser/parser.ts b/src/parser/parser.ts new file mode 100644 index 0000000..d00fb58 --- /dev/null +++ b/src/parser/parser.ts @@ -0,0 +1,175 @@ +import _ from "lodash"; + +import { + LinkDiscriminator, + SafeLink, + Link, + LinkContent, + Arguments, +} from "./types"; + +/* + * Verifiers are regexes which will match valid + * identifiers to their type + */ +type Verifier = [RegExp, A]; +export const roomVerifiers: Verifier< + LinkDiscriminator.Alias | LinkDiscriminator.RoomId +>[] = [ + [/^#([^\/:]+?):(.+)$/, LinkDiscriminator.Alias], + [/^!([^\/:]+?):(.+)$/, LinkDiscriminator.RoomId], +]; +export const verifiers: Verifier[] = [ + [/^[\!#]([^\/:]+?):(.+?)\/\$([^\/:]+?):(.+?)$/, LinkDiscriminator.Permalink], + [/^@([^\/:]+?):(.+)$/, LinkDiscriminator.UserId], + [/^\+([^\/:]+?):(.+)$/, LinkDiscriminator.GroupId], + ...roomVerifiers, +]; + +/* + * parseLink takes a striped hash link (without the '#/' prefix) + * and parses into a Link. If the parse failed the result will + * be ParseFailed + */ +export function parseLink(link: string): Link { + const [identifier, args] = link.split("?"); + + const kind = discriminate( + identifier, + verifiers, + LinkDiscriminator.ParseFailed + ); + const { vias, sharer, extras } = parseArgs(args); + + let parsedLink: LinkContent = { + identifier, + arguments: { + vias, + sharer, + extras, + }, + originalLink: link, + }; + + if (kind === LinkDiscriminator.Permalink) { + const { roomKind, roomLink, eventId } = parsePermalink(identifier); + + return { + kind, + ...parsedLink, + roomKind, + roomLink, + eventId, + }; + } + + return { + kind, + ...parsedLink, + }; +} + +/* + * Parses a permalink. + * Assumes the permalink is correct. + */ +export function parsePermalink(identifier: string) { + const [roomLink, eventId] = identifier.split("/"); + const roomKind = discriminate( + roomLink, + roomVerifiers, + // This is hacky but we're assuming identifier is a valid permalink + LinkDiscriminator.Alias + ); + + return { + roomKind, + roomLink, + eventId, + }; +} + +/* + * descriminate applies the verifiers to the identifier and + * returns it's type + */ +export function discriminate( + identifier: string, + verifiers: Verifier[], + fail: F +): T | F { + if (identifier !== encodeURI(identifier)) { + return fail; + } + + return verifiers.reduce((discriminator, verifier) => { + if (discriminator !== fail) { + return discriminator; + } + + if (identifier.match(verifier[0])) { + return verifier[1]; + } + + return discriminator; + }, fail); +} + +/* + * parseArgs parses the part of matrix.to links + */ +export function parseArgs(args: string): Arguments { + const parsedArgTuples = _.groupBy( + args + .split("&") + .map((x) => x.split("=")) + .filter((x) => x.length == 2), + (arg) => { + return arg[0]; + } + ); + + const parsedArgs = _.mapValues(parsedArgTuples, (arg) => + arg.map((x) => x[1]) + ); + + const { via, sharer, ...extras } = parsedArgs; + + return { + vias: via, + sharer: (parsedArgs.sharer || [undefined])[0], + extras, + }; +} + +/* + * toURI converts a parsed link to uri. Typically it's recommended + * to show the original link if it existed but this is handy in the + * case where this was constructed. + */ +export function toURI(hostname: string, link: SafeLink): string { + const cleanHostname = hostname.trim().replace(/\/+$/, ""); + switch (link.kind) { + case LinkDiscriminator.GroupId: + case LinkDiscriminator.UserId: + case LinkDiscriminator.RoomId: + case LinkDiscriminator.Alias: + case LinkDiscriminator.Permalink: + const uri = encodeURI(cleanHostname + "/#/" + link.identifier); + const vias = link.arguments.vias.map((s) => "via=" + s).join("&"); + const sharer = link.arguments.sharer + ? "sharer=" + link.arguments.sharer + : ""; + const extras = _.map(link.arguments.extras, (vals, key) => + vals.map((v) => key + "=" + v).join("&") + ).join("&"); + + const args = [vias, sharer, extras].filter(Boolean).join("&"); + + if (args) { + return uri + "?" + args; + } + + return uri; + } +} diff --git a/src/parser/types.ts b/src/parser/types.ts new file mode 100644 index 0000000..d765a55 --- /dev/null +++ b/src/parser/types.ts @@ -0,0 +1,55 @@ +import _ from "lodash"; + +export interface Arguments { + vias: string[]; + // Either one of the enums or a custom link + sharer: string; + extras: { [key: string]: string[] }; +} + +export interface LinkContent { + identifier: string; + arguments: Arguments; + originalLink: string; +} + +export enum LinkDiscriminator { + Alias = "ALIAS", + RoomId = "ROOM_ID", + UserId = "USER_ID", + Permalink = "PERMALINK", + GroupId = "GROUP_ID", + ParseFailed = "PARSE_FAILED", +} + +export interface Alias extends LinkContent { + kind: LinkDiscriminator.Alias; +} + +export interface RoomId extends LinkContent { + kind: LinkDiscriminator.RoomId; +} + +export interface UserId extends LinkContent { + kind: LinkDiscriminator.UserId; +} + +export interface GroupId extends LinkContent { + kind: LinkDiscriminator.GroupId; +} + +export interface Permalink extends LinkContent { + kind: LinkDiscriminator.Permalink; + roomKind: LinkDiscriminator.RoomId | LinkDiscriminator.Alias; + roomLink: string; + eventId: string; +} + +export interface ParseFailed { + kind: LinkDiscriminator.ParseFailed; + originalLink: string; +} + +export type SafeLink = Alias | RoomId | UserId | Permalink | GroupId; + +export type Link = SafeLink | ParseFailed; From 339ae069924edc50b1c7cd1fc0ffec512f15cace Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Thu, 11 Jun 2020 01:58:44 +0100 Subject: [PATCH 2/7] Force exhaustive checking on descriminator types --- tsconfig.json | 1 + 1 file changed, 1 insertion(+) diff --git a/tsconfig.json b/tsconfig.json index 277f285..195d86b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -16,6 +16,7 @@ "moduleResolution": "node", "resolveJsonModule": true, "isolatedModules": true, + "strictNullChecks": true, "noEmit": true, "jsx": "react" }, From a4aeefa53c1cb48fe9c59070c4d8231a90c126ff Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Thu, 11 Jun 2020 02:10:56 +0100 Subject: [PATCH 3/7] Doc lints --- src/parser/parser.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/parser/parser.ts b/src/parser/parser.ts index d00fb58..f863f59 100644 --- a/src/parser/parser.ts +++ b/src/parser/parser.ts @@ -10,7 +10,8 @@ import { /* * Verifiers are regexes which will match valid - * identifiers to their type + * identifiers to their type. (This is a lie, they + * can return anything) */ type Verifier = [RegExp, A]; export const roomVerifiers: Verifier< @@ -91,7 +92,7 @@ export function parsePermalink(identifier: string) { /* * descriminate applies the verifiers to the identifier and - * returns it's type + * returns the identifier's type */ export function discriminate( identifier: string, @@ -143,9 +144,9 @@ export function parseArgs(args: string): Arguments { } /* - * toURI converts a parsed link to uri. Typically it's recommended - * to show the original link if it existed but this is handy in the - * case where this was constructed. + * toURI converts a Link to uri. It's recommended + * to use the original link instead of toURI if it existed. + * This is handy function in case the Link was constructed. */ export function toURI(hostname: string, link: SafeLink): string { const cleanHostname = hostname.trim().replace(/\/+$/, ""); From d951d73de8115fa18d073b91df67b2eae7156e13 Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Tue, 16 Jun 2020 12:33:30 +0100 Subject: [PATCH 4/7] Add lodash dependency --- package.json | 2 ++ yarn.lock | 5 +++++ 2 files changed, 7 insertions(+) diff --git a/package.json b/package.json index 9852b9c..fbc8481 100644 --- a/package.json +++ b/package.json @@ -7,9 +7,11 @@ "@testing-library/react": "^9.3.2", "@testing-library/user-event": "^7.1.2", "@types/jest": "^24.0.0", + "@types/lodash": "^4.14.155", "@types/node": "^12.0.0", "@types/react": "^16.9.0", "@types/react-dom": "^16.9.0", + "lodash": "^4.17.15", "react": "^16.13.1", "react-dom": "^16.13.1", "react-scripts": "3.4.1", diff --git a/yarn.lock b/yarn.lock index 9b9b85b..ad87821 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1569,6 +1569,11 @@ resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.4.tgz#38fd73ddfd9b55abb1e1b2ed578cb55bd7b7d339" integrity sha512-8+KAKzEvSUdeo+kmqnKrqgeE+LcA0tjYWFY7RPProVYwnqDjukzO+3b6dLD56rYX5TdWejnEOLJYOIeh4CXKuA== +"@types/lodash@^4.14.155": + version "4.14.155" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.155.tgz#e2b4514f46a261fd11542e47519c20ebce7bc23a" + integrity sha512-vEcX7S7aPhsBCivxMwAANQburHBtfN9RdyXFk84IJmu2Z4Hkg1tOFgaslRiEqqvoLtbCBi6ika1EMspE+NZ9Lg== + "@types/minimatch@*": version "3.0.3" resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d" From 0aa273a8bc532527dc281e6a94765805d59deb97 Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Thu, 6 Aug 2020 14:49:33 +0100 Subject: [PATCH 5/7] Update parser to msc2644 and fix tests --- jest.config.js | 4 + package.json | 7 +- src/parser/parser.test.ts | 60 ++++++------- src/parser/parser.ts | 177 ++++++++++++++++++-------------------- src/parser/types.ts | 13 +-- yarn.lock | 131 +++++++++++++++++++++++----- 6 files changed, 236 insertions(+), 156 deletions(-) create mode 100644 jest.config.js diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 0000000..91a2d2c --- /dev/null +++ b/jest.config.js @@ -0,0 +1,4 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', +}; \ No newline at end of file diff --git a/package.json b/package.json index fbc8481..affdd14 100644 --- a/package.json +++ b/package.json @@ -6,8 +6,8 @@ "@testing-library/jest-dom": "^4.2.4", "@testing-library/react": "^9.3.2", "@testing-library/user-event": "^7.1.2", - "@types/jest": "^24.0.0", - "@types/lodash": "^4.14.155", + "@types/jest": "^26.0.9", + "@types/lodash": "^4.14.159", "@types/node": "^12.0.0", "@types/react": "^16.9.0", "@types/react-dom": "^16.9.0", @@ -54,6 +54,7 @@ "husky": "^4.2.5", "lint-staged": "^10.2.7", "node-sass": "^4.14.1", - "prettier": "^2.0.5" + "prettier": "^2.0.5", + "ts-jest": "^26.1.4" } } diff --git a/src/parser/parser.test.ts b/src/parser/parser.test.ts index 44b3314..31a80c6 100644 --- a/src/parser/parser.test.ts +++ b/src/parser/parser.test.ts @@ -1,42 +1,36 @@ import { - parseLink, + parseHash, parsePermalink, parseArgs, verifiers, - discriminate, - toURI, + identifyTypeFromRegex, + toURL, } from "./parser"; + import { LinkDiscriminator } from "./types"; -const curriedDiscriminate = (id: string) => - discriminate(id, verifiers, LinkDiscriminator.ParseFailed); +function identifierType(id: string) { + return identifyTypeFromRegex(id, verifiers, LinkDiscriminator.ParseFailed); +} it("types identifiers correctly", () => { - expect(curriedDiscriminate("@user:matrix.org")).toEqual( - LinkDiscriminator.UserId + expect(identifierType("@user:matrix.org")).toEqual(LinkDiscriminator.UserId); + expect(identifierType("!room:matrix.org")).toEqual(LinkDiscriminator.RoomId); + expect(identifierType("!somewhere:example.org/$event:example.org")).toEqual( + LinkDiscriminator.Permalink ); - expect(curriedDiscriminate("!room:matrix.org")).toEqual( - LinkDiscriminator.RoomId - ); - expect( - curriedDiscriminate("!somewhere:example.org/$event:example.org") - ).toEqual(LinkDiscriminator.Permalink); - expect(curriedDiscriminate("+group:matrix.org")).toEqual( + expect(identifierType("+group:matrix.org")).toEqual( LinkDiscriminator.GroupId ); - expect(curriedDiscriminate("#alias:matrix.org")).toEqual( - LinkDiscriminator.Alias - ); + expect(identifierType("#alias:matrix.org")).toEqual(LinkDiscriminator.Alias); }); it("types garbadge as such", () => { - expect(curriedDiscriminate("sdfa;fdlkja")).toEqual( + expect(identifierType("sdfa;fdlkja")).toEqual(LinkDiscriminator.ParseFailed); + expect(identifierType("$event$matrix.org")).toEqual( LinkDiscriminator.ParseFailed ); - expect(curriedDiscriminate("$event$matrix.org")).toEqual( - LinkDiscriminator.ParseFailed - ); - expect(curriedDiscriminate("/user:matrix.org")).toEqual( + expect(identifierType("/user:matrix.org")).toEqual( LinkDiscriminator.ParseFailed ); }); @@ -51,11 +45,13 @@ it("parses sharer", () => { expect(parseArgs("sharer=blah")).toHaveProperty("sharer", "blah"); }); -it("parses random args", () => { - expect(parseArgs("via=qreqrqwer&banter=2342")).toHaveProperty( - "extras.banter", - ["2342"] - ); +it("parses client", () => { + expect(parseArgs("client=blah.com")).toHaveProperty("client", "blah.com"); +}); + +it("parses federated", () => { + expect(parseArgs("federated=true")).toHaveProperty("federated", true); + expect(parseArgs("federated=false")).toHaveProperty("federated", false); }); it("parses permalinks", () => { @@ -68,15 +64,15 @@ it("parses permalinks", () => { it("formats links correctly", () => { const bigLink = - "!somewhere:example.org/$event:example.org?via=dfasdf&via=jfjafjaf&uselesstag=useless"; - const host = "matrix.org"; - const prefix = host + "/#/"; - const parse = parseLink(bigLink); + "!somewhere:example.org/$event:example.org?via=dfasdf&via=jfjafjaf"; + const origin = "https://matrix.org"; + const prefix = origin + "/#/"; + const parse = parseHash(bigLink); switch (parse.kind) { case LinkDiscriminator.ParseFailed: fail("Parse failed"); default: - expect(toURI(host, parse)).toEqual(prefix + bigLink); + expect(toURL(origin, parse).toString()).toEqual(prefix + bigLink); } }); diff --git a/src/parser/parser.ts b/src/parser/parser.ts index f863f59..0ffea62 100644 --- a/src/parser/parser.ts +++ b/src/parser/parser.ts @@ -1,4 +1,4 @@ -import _ from "lodash"; +import forEach from "lodash/forEach"; import { LinkDiscriminator, @@ -9,47 +9,23 @@ import { } from "./types"; /* - * Verifiers are regexes which will match valid - * identifiers to their type. (This is a lie, they - * can return anything) - */ -type Verifier = [RegExp, A]; -export const roomVerifiers: Verifier< - LinkDiscriminator.Alias | LinkDiscriminator.RoomId ->[] = [ - [/^#([^\/:]+?):(.+)$/, LinkDiscriminator.Alias], - [/^!([^\/:]+?):(.+)$/, LinkDiscriminator.RoomId], -]; -export const verifiers: Verifier[] = [ - [/^[\!#]([^\/:]+?):(.+?)\/\$([^\/:]+?):(.+?)$/, LinkDiscriminator.Permalink], - [/^@([^\/:]+?):(.+)$/, LinkDiscriminator.UserId], - [/^\+([^\/:]+?):(.+)$/, LinkDiscriminator.GroupId], - ...roomVerifiers, -]; - -/* - * parseLink takes a striped hash link (without the '#/' prefix) + * parseLink takes a striped matrix.to hash link (without the '#/' prefix) * and parses into a Link. If the parse failed the result will * be ParseFailed */ -export function parseLink(link: string): Link { - const [identifier, args] = link.split("?"); +export function parseHash(hash: string): Link { + const [identifier, args] = hash.split("?"); - const kind = discriminate( + const kind = identifyTypeFromRegex( identifier, verifiers, LinkDiscriminator.ParseFailed ); - const { vias, sharer, extras } = parseArgs(args); let parsedLink: LinkContent = { identifier, - arguments: { - vias, - sharer, - extras, - }, - originalLink: link, + arguments: parseArgs(args), + originalLink: hash, }; if (kind === LinkDiscriminator.Permalink) { @@ -76,7 +52,7 @@ export function parseLink(link: string): Link { */ export function parsePermalink(identifier: string) { const [roomLink, eventId] = identifier.split("/"); - const roomKind = discriminate( + const roomKind = identifyTypeFromRegex( roomLink, roomVerifiers, // This is hacky but we're assuming identifier is a valid permalink @@ -91,10 +67,84 @@ export function parsePermalink(identifier: string) { } /* - * descriminate applies the verifiers to the identifier and + * parseArgs parses the part of matrix.to links + */ +export function parseArgs(args: string): Arguments { + const params = new URLSearchParams(args); + const _federated = params.get("federated"); + const federated = _federated !== null ? _federated === "true" : null; + + return { + vias: params.getAll("via"), + federated: bottomExchange(federated), + client: bottomExchange(params.get("client")), + sharer: bottomExchange(params.get("sharer")), + }; +} + +/* + * Repalces null with undefined + */ +function bottomExchange(nullable: T | null): T | undefined { + if (nullable === null) return undefined; + return nullable; +} + +/* + * toURI converts a Link to a url. It's recommended + * to use the original link instead of toURI if it existed. + * This is handy function in case the Link was constructed. + */ +export function toURL(origin: string, link: SafeLink): URL { + switch (link.kind) { + case LinkDiscriminator.GroupId: + case LinkDiscriminator.UserId: + case LinkDiscriminator.RoomId: + case LinkDiscriminator.Alias: + case LinkDiscriminator.Permalink: + const params = new URLSearchParams(); + forEach(link.arguments, (value, key) => { + if (value === undefined) { + // do nothing + } else if (key === "vias") { + ((value)).forEach((via) => + params.append("via", via) + ); + } else { + params.append(key, value.toString()); + } + }); + + const url = new URL(origin); + url.hash = `/${link.identifier}?${params.toString()}`; + return url; + } +} + +/* + * Verifiers are regexes which will match valid + * identifiers to their type. (This is a lie, they + * can return anything) + */ +type Verifier = [RegExp, A]; +export const roomVerifiers: Verifier< + LinkDiscriminator.Alias | LinkDiscriminator.RoomId +>[] = [ + [/^#([^\/:]+?):(.+)$/, LinkDiscriminator.Alias], + [/^!([^\/:]+?):(.+)$/, LinkDiscriminator.RoomId], +]; +export const verifiers: Verifier[] = [ + [/^[\!#]([^\/:]+?):(.+?)\/\$([^\/:]+?):(.+?)$/, LinkDiscriminator.Permalink], + [/^@([^\/:]+?):(.+)$/, LinkDiscriminator.UserId], + [/^\+([^\/:]+?):(.+)$/, LinkDiscriminator.GroupId], + ...roomVerifiers, +]; + +/* + * identifyTypeFromRegex applies the verifiers to the identifier and * returns the identifier's type */ -export function discriminate( +export function identifyTypeFromRegex( identifier: string, verifiers: Verifier[], fail: F @@ -115,62 +165,3 @@ export function discriminate( return discriminator; }, fail); } - -/* - * parseArgs parses the part of matrix.to links - */ -export function parseArgs(args: string): Arguments { - const parsedArgTuples = _.groupBy( - args - .split("&") - .map((x) => x.split("=")) - .filter((x) => x.length == 2), - (arg) => { - return arg[0]; - } - ); - - const parsedArgs = _.mapValues(parsedArgTuples, (arg) => - arg.map((x) => x[1]) - ); - - const { via, sharer, ...extras } = parsedArgs; - - return { - vias: via, - sharer: (parsedArgs.sharer || [undefined])[0], - extras, - }; -} - -/* - * toURI converts a Link to uri. It's recommended - * to use the original link instead of toURI if it existed. - * This is handy function in case the Link was constructed. - */ -export function toURI(hostname: string, link: SafeLink): string { - const cleanHostname = hostname.trim().replace(/\/+$/, ""); - switch (link.kind) { - case LinkDiscriminator.GroupId: - case LinkDiscriminator.UserId: - case LinkDiscriminator.RoomId: - case LinkDiscriminator.Alias: - case LinkDiscriminator.Permalink: - const uri = encodeURI(cleanHostname + "/#/" + link.identifier); - const vias = link.arguments.vias.map((s) => "via=" + s).join("&"); - const sharer = link.arguments.sharer - ? "sharer=" + link.arguments.sharer - : ""; - const extras = _.map(link.arguments.extras, (vals, key) => - vals.map((v) => key + "=" + v).join("&") - ).join("&"); - - const args = [vias, sharer, extras].filter(Boolean).join("&"); - - if (args) { - return uri + "?" + args; - } - - return uri; - } -} diff --git a/src/parser/types.ts b/src/parser/types.ts index d765a55..f629b90 100644 --- a/src/parser/types.ts +++ b/src/parser/types.ts @@ -1,10 +1,13 @@ -import _ from "lodash"; - export interface Arguments { vias: string[]; - // Either one of the enums or a custom link - sharer: string; - extras: { [key: string]: string[] }; + // Schemeless http identifier + client?: string; + // Indicates whether a room exists on a federating server (assumed to be the + // default), or if the client must connect via the server identified by the + // room ID or event ID + federated?: boolean; + // MXID + sharer?: string; } export interface LinkContent { diff --git a/yarn.lock b/yarn.lock index ad87821..ed579d3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1317,6 +1317,17 @@ "@types/yargs" "^15.0.0" chalk "^3.0.0" +"@jest/types@^26.2.0": + version "26.2.0" + resolved "https://registry.yarnpkg.com/@jest/types/-/types-26.2.0.tgz#b28ca1fb517a4eb48c0addea7fcd9edc4ab45721" + integrity sha512-lvm3rJvctxd7+wxKSxxbzpDbr4FXDLaC57WEKdUIZ2cjTYuxYSc0zlyD7Z4Uqr5VdKxRUrtwIkiqBuvgf8uKJA== + dependencies: + "@types/istanbul-lib-coverage" "^2.0.0" + "@types/istanbul-reports" "^1.1.1" + "@types/node" "*" + "@types/yargs" "^15.0.0" + chalk "^4.0.0" + "@mrmlnc/readdir-enhanced@^2.2.1": version "2.2.1" resolved "https://registry.yarnpkg.com/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz#524af240d1a360527b730475ecfa1344aa540dde" @@ -1557,22 +1568,23 @@ "@types/istanbul-lib-coverage" "*" "@types/istanbul-lib-report" "*" -"@types/jest@^24.0.0": - version "24.9.1" - resolved "https://registry.yarnpkg.com/@types/jest/-/jest-24.9.1.tgz#02baf9573c78f1b9974a5f36778b366aa77bd534" - integrity sha512-Fb38HkXSVA4L8fGKEZ6le5bB8r6MRWlOCZbVuWZcmOMSCd2wCYOwN1ibj8daIoV9naq7aaOZjrLCoCMptKU/4Q== +"@types/jest@^26.0.9": + version "26.0.9" + resolved "https://registry.yarnpkg.com/@types/jest/-/jest-26.0.9.tgz#0543b57da5f0cd949c5f423a00c56c492289c989" + integrity sha512-k4qFfJ5AUKrWok5KYXp2EPm89b0P/KZpl7Vg4XuOTVVQEhLDBDBU3iBFrjjdgd8fLw96aAtmnwhXHl63bWeBQQ== dependencies: - jest-diff "^24.3.0" + jest-diff "^25.2.1" + pretty-format "^25.2.1" "@types/json-schema@^7.0.3", "@types/json-schema@^7.0.4": version "7.0.4" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.4.tgz#38fd73ddfd9b55abb1e1b2ed578cb55bd7b7d339" integrity sha512-8+KAKzEvSUdeo+kmqnKrqgeE+LcA0tjYWFY7RPProVYwnqDjukzO+3b6dLD56rYX5TdWejnEOLJYOIeh4CXKuA== -"@types/lodash@^4.14.155": - version "4.14.155" - resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.155.tgz#e2b4514f46a261fd11542e47519c20ebce7bc23a" - integrity sha512-vEcX7S7aPhsBCivxMwAANQburHBtfN9RdyXFk84IJmu2Z4Hkg1tOFgaslRiEqqvoLtbCBi6ika1EMspE+NZ9Lg== +"@types/lodash@^4.14.159": + version "4.14.159" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.159.tgz#61089719dc6fdd9c5cb46efc827f2571d1517065" + integrity sha512-gF7A72f7WQN33DpqOWw9geApQPh4M3PxluMtaHxWHXEGSN12/WbcEk/eNSqWNQcQhF66VSZ06vCF94CrHwXJDg== "@types/minimatch@*": version "3.0.3" @@ -2708,6 +2720,13 @@ browserslist@^4.0.0, browserslist@^4.12.0, browserslist@^4.6.2, browserslist@^4. node-releases "^1.1.53" pkg-up "^2.0.0" +bs-logger@0.x: + version "0.2.6" + resolved "https://registry.yarnpkg.com/bs-logger/-/bs-logger-0.2.6.tgz#eb7d365307a72cf974cc6cda76b68354ad336bd8" + integrity sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog== + dependencies: + fast-json-stable-stringify "2.x" + bser@2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/bser/-/bser-2.1.1.tgz#e6787da20ece9d07998533cfd9de6f5c38f4bc05" @@ -2715,7 +2734,7 @@ bser@2.1.1: dependencies: node-int64 "^0.4.0" -buffer-from@^1.0.0: +buffer-from@1.x, buffer-from@^1.0.0: version "1.1.1" resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A== @@ -3873,6 +3892,11 @@ diff-sequences@^24.9.0: resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-24.9.0.tgz#5715d6244e2aa65f48bba0bc972db0b0b11e95b5" integrity sha512-Dj6Wk3tWyTE+Fo1rW8v0Xhwk80um6yFYKbuAxc9c3EZxIHFDYwbi34Uk42u1CdnIiVorvt4RmlSDjIPyzGC2ew== +diff-sequences@^25.2.6: + version "25.2.6" + resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-25.2.6.tgz#5f467c00edd35352b7bca46d7927d60e687a76dd" + integrity sha512-Hq8o7+6GaZeoFjtpgvRBUknSXNeJiCx7V9Fr94ZMljNiCr9n9L8H8aJqgWOQiDDGdyn29fRNcDdRVJ5fdyihfg== + diffie-hellman@^5.0.0: version "5.0.3" resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.3.tgz#40e8ee98f55a2149607146921c63e1ae5f3d2875" @@ -4636,7 +4660,7 @@ fast-glob@^2.0.2: merge2 "^1.2.3" micromatch "^3.1.10" -fast-json-stable-stringify@^2.0.0: +fast-json-stable-stringify@2.x, fast-json-stable-stringify@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== @@ -5154,7 +5178,7 @@ globule@^1.0.0: lodash "~4.17.12" minimatch "~3.0.2" -graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.2: +graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.2, graceful-fs@^4.2.4: version "4.2.4" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb" integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw== @@ -6143,7 +6167,7 @@ jest-config@^24.9.0: pretty-format "^24.9.0" realpath-native "^1.1.0" -jest-diff@^24.0.0, jest-diff@^24.3.0, jest-diff@^24.9.0: +jest-diff@^24.0.0, jest-diff@^24.9.0: version "24.9.0" resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-24.9.0.tgz#931b7d0d5778a1baf7452cb816e325e3724055da" integrity sha512-qMfrTs8AdJE2iqrTp0hzh7kTd2PQWrsFyj9tORoKmu32xjPjeE4NyjVRDz8ybYwqS2ik8N4hsIpiVTyFeo2lBQ== @@ -6153,6 +6177,16 @@ jest-diff@^24.0.0, jest-diff@^24.3.0, jest-diff@^24.9.0: jest-get-type "^24.9.0" pretty-format "^24.9.0" +jest-diff@^25.2.1: + version "25.5.0" + resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-25.5.0.tgz#1dd26ed64f96667c068cef026b677dfa01afcfa9" + integrity sha512-z1kygetuPiREYdNIumRpAHY6RXiGmp70YHptjdaxTWGmA085W3iCnXNx0DhflK3vwrKmrRWyY1wUpkPMVxMK7A== + dependencies: + chalk "^3.0.0" + diff-sequences "^25.2.6" + jest-get-type "^25.2.6" + pretty-format "^25.5.0" + jest-docblock@^24.3.0: version "24.9.0" resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-24.9.0.tgz#7970201802ba560e1c4092cc25cbedf5af5a8ce2" @@ -6211,6 +6245,11 @@ jest-get-type@^24.9.0: resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-24.9.0.tgz#1684a0c8a50f2e4901b6644ae861f579eed2ef0e" integrity sha512-lUseMzAley4LhIcpSP9Jf+fTrQ4a1yHQwLNeeVa2cEmbCGeoZAtYPOIv8JaxLD/sUpKxetKGP+gsHl8f8TSj8Q== +jest-get-type@^25.2.6: + version "25.2.6" + resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-25.2.6.tgz#0b0a32fab8908b44d508be81681487dbabb8d877" + integrity sha512-DxjtyzOHjObRM+sM1knti6or+eOgcGU4xVSb2HNP1TqO4ahsT+rqZg+nyqHWJSvWgKC5cG3QjGFBqxLghiF/Ig== + jest-haste-map@^24.9.0: version "24.9.0" resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-24.9.0.tgz#b38a5d64274934e21fa417ae9a9fbeb77ceaac7d" @@ -6399,6 +6438,18 @@ jest-snapshot@^24.9.0: pretty-format "^24.9.0" semver "^6.2.0" +jest-util@26.x: + version "26.2.0" + resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-26.2.0.tgz#0597d2a27c559340957609f106c408c17c1d88ac" + integrity sha512-YmDwJxLZ1kFxpxPfhSJ0rIkiZOM0PQbRcfH0TzJOhqCisCAsI1WcmoQqO83My9xeVA2k4n+rzg2UuexVKzPpig== + dependencies: + "@jest/types" "^26.2.0" + "@types/node" "*" + chalk "^4.0.0" + graceful-fs "^4.2.4" + is-ci "^2.0.0" + micromatch "^4.0.2" + jest-util@^24.0.0, jest-util@^24.9.0: version "24.9.0" resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-24.9.0.tgz#7396814e48536d2e85a37de3e4c431d7cb140162" @@ -6618,6 +6669,13 @@ json3@^3.3.2: resolved "https://registry.yarnpkg.com/json3/-/json3-3.3.3.tgz#7fc10e375fc5ae42c4705a5cc0aa6f62be305b81" integrity sha512-c7/8mbUsKigAbLkD5B010BK4D9LZm7A1pNItkEwiUZRpIN66exu/e7YQWysGun+TRKaJp8MhemM+VkfWv42aCA== +json5@2.x, json5@^2.1.2: + version "2.1.3" + resolved "https://registry.yarnpkg.com/json5/-/json5-2.1.3.tgz#c9b0f7fa9233bfe5807fe66fcf3a5617ed597d43" + integrity sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA== + dependencies: + minimist "^1.2.5" + json5@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.1.tgz#779fb0018604fa854eacbf6252180d83543e3dbe" @@ -6625,13 +6683,6 @@ json5@^1.0.1: dependencies: minimist "^1.2.0" -json5@^2.1.2: - version "2.1.3" - resolved "https://registry.yarnpkg.com/json5/-/json5-2.1.3.tgz#c9b0f7fa9233bfe5807fe66fcf3a5617ed597d43" - integrity sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA== - dependencies: - minimist "^1.2.5" - jsonfile@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb" @@ -6882,7 +6933,7 @@ lodash._reinterpolate@^3.0.0: resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d" integrity sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0= -lodash.memoize@^4.1.2: +lodash.memoize@4.x, lodash.memoize@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" integrity sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4= @@ -6991,6 +7042,11 @@ make-dir@^3.0.2: dependencies: semver "^6.0.0" +make-error@1.x: + version "1.3.6" + resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" + integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== + makeerror@1.0.x: version "1.0.11" resolved "https://registry.yarnpkg.com/makeerror/-/makeerror-1.0.11.tgz#e01a5c9109f2af79660e4e8b9587790184f5a96c" @@ -7285,6 +7341,11 @@ mixin-object@^2.0.1: for-in "^0.1.3" is-extendable "^0.1.1" +mkdirp@1.x: + version "1.0.4" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" + integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== + "mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@^0.5.3, mkdirp@~0.5.1: version "0.5.5" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def" @@ -8912,7 +8973,7 @@ pretty-format@^24.0.0, pretty-format@^24.3.0, pretty-format@^24.9.0: ansi-styles "^3.2.0" react-is "^16.8.4" -pretty-format@^25.1.0: +pretty-format@^25.1.0, pretty-format@^25.2.1, pretty-format@^25.5.0: version "25.5.0" resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-25.5.0.tgz#7873c1d774f682c34b8d48b6743a2bf2ac55791a" integrity sha512-kbo/kq2LQ/A/is0PQwsEHM7Ca6//bGPPvU6UnsdDRSKTWxT/ru/xb88v4BJf6a69H+uTytOEsTusT9ksd/1iWQ== @@ -9846,7 +9907,7 @@ semver@7.0.0: resolved "https://registry.yarnpkg.com/semver/-/semver-7.0.0.tgz#5f3ca35761e47e05b206c6daff2cf814f0316b8e" integrity sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A== -semver@^7.3.2: +semver@7.x, semver@^7.3.2: version "7.3.2" resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.2.tgz#604962b052b81ed0786aae84389ffba70ffd3938" integrity sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ== @@ -10805,6 +10866,22 @@ trim-newlines@^1.0.0: dependencies: glob "^7.1.2" +ts-jest@^26.1.4: + version "26.1.4" + resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-26.1.4.tgz#87d41a96016a8efe4b8cc14501d3785459af6fa6" + integrity sha512-Nd7diUX6NZWfWq6FYyvcIPR/c7GbEF75fH1R6coOp3fbNzbRJBZZAn0ueVS0r8r9ral1VcrpneAFAwB3TsVS1Q== + dependencies: + bs-logger "0.x" + buffer-from "1.x" + fast-json-stable-stringify "2.x" + jest-util "26.x" + json5 "2.x" + lodash.memoize "4.x" + make-error "1.x" + mkdirp "1.x" + semver "7.x" + yargs-parser "18.x" + ts-pnp@1.1.6: version "1.1.6" resolved "https://registry.yarnpkg.com/ts-pnp/-/ts-pnp-1.1.6.tgz#389a24396d425a0d3162e96d2b4638900fdc289a" @@ -11610,6 +11687,14 @@ yaml@^1.7.2: resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.0.tgz#3b593add944876077d4d683fee01081bd9fff31e" integrity sha512-yr2icI4glYaNG+KWONODapy2/jDdMSDnrONSjblABjD9B4Z5LgiircSt8m8sRZFNi08kG9Sm0uSHtEmP3zaEGg== +yargs-parser@18.x: + version "18.1.3" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0" + integrity sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ== + dependencies: + camelcase "^5.0.0" + decamelize "^1.2.0" + yargs-parser@^11.1.1: version "11.1.1" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-11.1.1.tgz#879a0865973bca9f6bab5cbdf3b1c67ec7d3bcf4" From 14e22d41dca2e321cbb8b585187c5aca44553629 Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Mon, 10 Aug 2020 11:57:54 +0100 Subject: [PATCH 6/7] Fix Typo Co-authored-by: J. Ryan Stinnett --- src/parser/parser.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/parser/parser.test.ts b/src/parser/parser.test.ts index 31a80c6..873fa5a 100644 --- a/src/parser/parser.test.ts +++ b/src/parser/parser.test.ts @@ -25,7 +25,7 @@ it("types identifiers correctly", () => { expect(identifierType("#alias:matrix.org")).toEqual(LinkDiscriminator.Alias); }); -it("types garbadge as such", () => { +it("types garbage as such", () => { expect(identifierType("sdfa;fdlkja")).toEqual(LinkDiscriminator.ParseFailed); expect(identifierType("$event$matrix.org")).toEqual( LinkDiscriminator.ParseFailed From 5b2c36833ce834466cc93e734551bba091e05beb Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Mon, 10 Aug 2020 12:35:15 +0100 Subject: [PATCH 7/7] Remove federtaed argument --- src/parser/parser.test.ts | 104 +++++++-------- src/parser/parser.ts | 274 +++++++++++++++++++------------------- src/parser/types.ts | 54 ++++---- 3 files changed, 205 insertions(+), 227 deletions(-) diff --git a/src/parser/parser.test.ts b/src/parser/parser.test.ts index 873fa5a..758dce2 100644 --- a/src/parser/parser.test.ts +++ b/src/parser/parser.test.ts @@ -1,78 +1,64 @@ +/* eslint-disable no-fallthrough */ + import { - parseHash, - parsePermalink, - parseArgs, - verifiers, - identifyTypeFromRegex, - toURL, + parseHash, + parsePermalink, + parseArgs, + verifiers, + identifyTypeFromRegex, + toURL, } from "./parser"; -import { LinkDiscriminator } from "./types"; +import { LinkKind } from "./types"; -function identifierType(id: string) { - return identifyTypeFromRegex(id, verifiers, LinkDiscriminator.ParseFailed); -} +const identifierType = (id: string): LinkKind => + identifyTypeFromRegex(id, verifiers, LinkKind.ParseFailed); it("types identifiers correctly", () => { - expect(identifierType("@user:matrix.org")).toEqual(LinkDiscriminator.UserId); - expect(identifierType("!room:matrix.org")).toEqual(LinkDiscriminator.RoomId); - expect(identifierType("!somewhere:example.org/$event:example.org")).toEqual( - LinkDiscriminator.Permalink - ); - expect(identifierType("+group:matrix.org")).toEqual( - LinkDiscriminator.GroupId - ); - expect(identifierType("#alias:matrix.org")).toEqual(LinkDiscriminator.Alias); + expect(identifierType("@user:matrix.org")).toEqual(LinkKind.UserId); + expect(identifierType("!room:matrix.org")).toEqual(LinkKind.RoomId); + expect(identifierType("!somewhere:example.org/$event:example.org")).toEqual( + LinkKind.Permalink + ); + expect(identifierType("+group:matrix.org")).toEqual(LinkKind.GroupId); + expect(identifierType("#alias:matrix.org")).toEqual(LinkKind.Alias); }); it("types garbage as such", () => { - expect(identifierType("sdfa;fdlkja")).toEqual(LinkDiscriminator.ParseFailed); - expect(identifierType("$event$matrix.org")).toEqual( - LinkDiscriminator.ParseFailed - ); - expect(identifierType("/user:matrix.org")).toEqual( - LinkDiscriminator.ParseFailed - ); + expect(identifierType("sdfa;fdlkja")).toEqual(LinkKind.ParseFailed); + expect(identifierType("$event$matrix.org")).toEqual(LinkKind.ParseFailed); + expect(identifierType("/user:matrix.org")).toEqual(LinkKind.ParseFailed); }); -it("parses vias", () => { - expect( - parseArgs("via=example.org&via=alt.example.org") - ).toHaveProperty("vias", ["example.org", "alt.example.org"]); -}); - -it("parses sharer", () => { - expect(parseArgs("sharer=blah")).toHaveProperty("sharer", "blah"); -}); - -it("parses client", () => { - expect(parseArgs("client=blah.com")).toHaveProperty("client", "blah.com"); -}); - -it("parses federated", () => { - expect(parseArgs("federated=true")).toHaveProperty("federated", true); - expect(parseArgs("federated=false")).toHaveProperty("federated", false); +it("parses args correctly", () => { + expect( + parseArgs("via=example.org&via=alt.example.org") + ).toHaveProperty("vias", ["example.org", "alt.example.org"]); + expect(parseArgs("sharer=blah")).toHaveProperty("sharer", "blah"); + expect(parseArgs("client=blah.com")).toHaveProperty("client", "blah.com"); }); it("parses permalinks", () => { - expect(parsePermalink("!somewhere:example.org/$event:example.org")).toEqual({ - roomKind: LinkDiscriminator.RoomId, - roomLink: "!somewhere:example.org", - eventId: "$event:example.org", - }); + expect(parsePermalink("!somewhere:example.org/$event:example.org")).toEqual( + { + roomKind: LinkKind.RoomId, + roomLink: "!somewhere:example.org", + eventId: "$event:example.org", + } + ); }); it("formats links correctly", () => { - const bigLink = - "!somewhere:example.org/$event:example.org?via=dfasdf&via=jfjafjaf"; - const origin = "https://matrix.org"; - const prefix = origin + "/#/"; - const parse = parseHash(bigLink); + const bigLink = + "!somewhere:example.org/$event:example.org?via=dfasdf&via=jfjafjaf"; + const origin = "https://matrix.org"; + const prefix = origin + "/#/"; + const parse = parseHash(bigLink); - switch (parse.kind) { - case LinkDiscriminator.ParseFailed: - fail("Parse failed"); - default: - expect(toURL(origin, parse).toString()).toEqual(prefix + bigLink); - } + switch (parse.kind) { + case LinkKind.ParseFailed: + fail("Parse failed"); + default: + expect(toURL(origin, parse).toString()).toEqual(prefix + bigLink); + } }); diff --git a/src/parser/parser.ts b/src/parser/parser.ts index 0ffea62..59a9a57 100644 --- a/src/parser/parser.ts +++ b/src/parser/parser.ts @@ -1,93 +1,134 @@ import forEach from "lodash/forEach"; import { - LinkDiscriminator, - SafeLink, - Link, - LinkContent, - Arguments, + LinkKind, + SafeLink, + Link, + LinkContent, + Arguments, + Permalink, } from "./types"; +/* + * Verifiers are regexes which will match valid + * identifiers to their type. (This is a lie, they + * can return anything) + */ +type Verifier = [RegExp, A]; +export const roomVerifiers: Verifier[] = [ + [/^#([^/:]+?):(.+)$/, LinkKind.Alias], + [/^!([^/:]+?):(.+)$/, LinkKind.RoomId], +]; +export const verifiers: Verifier[] = [ + [/^[!#]([^/:]+?):(.+?)\/\$([^/:]+?):(.+?)$/, LinkKind.Permalink], + [/^@([^/:]+?):(.+)$/, LinkKind.UserId], + [/^\+([^/:]+?):(.+)$/, LinkKind.GroupId], + ...roomVerifiers, +]; + +/* + * identifyTypeFromRegex applies the verifiers to the identifier and + * returns the identifier's type + */ +export function identifyTypeFromRegex( + identifier: string, + verifiers: Verifier[], + fail: F +): T | F { + if (identifier !== encodeURI(identifier)) { + return fail; + } + + return verifiers.reduce((kind, verifier) => { + if (kind !== fail) { + return kind; + } + + if (identifier.match(verifier[0])) { + return verifier[1]; + } + + return kind; + }, fail); +} + +/* + * Parses a permalink. + * Assumes the permalink is correct. + */ +export function parsePermalink(identifier: string): Permalink { + const [roomLink, eventId] = identifier.split("/"); + const roomKind = identifyTypeFromRegex( + roomLink, + roomVerifiers, + // This is hacky but we're assuming identifier is a valid permalink + LinkKind.Alias + ); + + return { + roomKind, + roomLink, + eventId, + }; +} + +/* + * Repalces null with undefined + */ +function bottomExchange(nullable: T | null): T | undefined { + if (nullable === null) return undefined; + return nullable; +} + +/* + * parseArgs parses the part of matrix.to links + */ +export function parseArgs(args: string): Arguments { + const params = new URLSearchParams(args); + + return { + vias: params.getAll("via"), + client: bottomExchange(params.get("client")), + sharer: bottomExchange(params.get("sharer")), + }; +} + /* * parseLink takes a striped matrix.to hash link (without the '#/' prefix) * and parses into a Link. If the parse failed the result will * be ParseFailed */ export function parseHash(hash: string): Link { - const [identifier, args] = hash.split("?"); + const [identifier, args] = hash.split("?"); - const kind = identifyTypeFromRegex( - identifier, - verifiers, - LinkDiscriminator.ParseFailed - ); + const kind = identifyTypeFromRegex( + identifier, + verifiers, + LinkKind.ParseFailed + ); - let parsedLink: LinkContent = { - identifier, - arguments: parseArgs(args), - originalLink: hash, - }; + const parsedLink: LinkContent = { + identifier, + arguments: parseArgs(args), + originalLink: hash, + }; - if (kind === LinkDiscriminator.Permalink) { - const { roomKind, roomLink, eventId } = parsePermalink(identifier); + if (kind === LinkKind.Permalink) { + const { roomKind, roomLink, eventId } = parsePermalink(identifier); + + return { + kind, + ...parsedLink, + roomKind, + roomLink, + eventId, + }; + } return { - kind, - ...parsedLink, - roomKind, - roomLink, - eventId, + kind, + ...parsedLink, }; - } - - return { - kind, - ...parsedLink, - }; -} - -/* - * Parses a permalink. - * Assumes the permalink is correct. - */ -export function parsePermalink(identifier: string) { - const [roomLink, eventId] = identifier.split("/"); - const roomKind = identifyTypeFromRegex( - roomLink, - roomVerifiers, - // This is hacky but we're assuming identifier is a valid permalink - LinkDiscriminator.Alias - ); - - return { - roomKind, - roomLink, - eventId, - }; -} - -/* - * parseArgs parses the part of matrix.to links - */ -export function parseArgs(args: string): Arguments { - const params = new URLSearchParams(args); - const _federated = params.get("federated"); - const federated = _federated !== null ? _federated === "true" : null; - - return { - vias: params.getAll("via"), - federated: bottomExchange(federated), - client: bottomExchange(params.get("client")), - sharer: bottomExchange(params.get("sharer")), - }; -} - -/* - * Repalces null with undefined - */ -function bottomExchange(nullable: T | null): T | undefined { - if (nullable === null) return undefined; - return nullable; } /* @@ -96,72 +137,27 @@ function bottomExchange(nullable: T | null): T | undefined { * This is handy function in case the Link was constructed. */ export function toURL(origin: string, link: SafeLink): URL { - switch (link.kind) { - case LinkDiscriminator.GroupId: - case LinkDiscriminator.UserId: - case LinkDiscriminator.RoomId: - case LinkDiscriminator.Alias: - case LinkDiscriminator.Permalink: - const params = new URLSearchParams(); - forEach(link.arguments, (value, key) => { - if (value === undefined) { - // do nothing - } else if (key === "vias") { - ((value)).forEach((via) => - params.append("via", via) - ); - } else { - params.append(key, value.toString()); - } - }); + const params = new URLSearchParams(); + const url = new URL(origin); + switch (link.kind) { + case LinkKind.GroupId: + case LinkKind.UserId: + case LinkKind.RoomId: + case LinkKind.Alias: + case LinkKind.Permalink: + forEach(link.arguments, (value, key) => { + if (value === undefined) { + // do nothing + } else if (key === "vias") { + (value as string[]).forEach((via) => + params.append("via", via) + ); + } else { + params.append(key, value.toString()); + } + }); - const url = new URL(origin); - url.hash = `/${link.identifier}?${params.toString()}`; - return url; - } -} - -/* - * Verifiers are regexes which will match valid - * identifiers to their type. (This is a lie, they - * can return anything) - */ -type Verifier = [RegExp, A]; -export const roomVerifiers: Verifier< - LinkDiscriminator.Alias | LinkDiscriminator.RoomId ->[] = [ - [/^#([^\/:]+?):(.+)$/, LinkDiscriminator.Alias], - [/^!([^\/:]+?):(.+)$/, LinkDiscriminator.RoomId], -]; -export const verifiers: Verifier[] = [ - [/^[\!#]([^\/:]+?):(.+?)\/\$([^\/:]+?):(.+?)$/, LinkDiscriminator.Permalink], - [/^@([^\/:]+?):(.+)$/, LinkDiscriminator.UserId], - [/^\+([^\/:]+?):(.+)$/, LinkDiscriminator.GroupId], - ...roomVerifiers, -]; - -/* - * identifyTypeFromRegex applies the verifiers to the identifier and - * returns the identifier's type - */ -export function identifyTypeFromRegex( - identifier: string, - verifiers: Verifier[], - fail: F -): T | F { - if (identifier !== encodeURI(identifier)) { - return fail; - } - - return verifiers.reduce((discriminator, verifier) => { - if (discriminator !== fail) { - return discriminator; + url.hash = `/${link.identifier}?${params.toString()}`; } - - if (identifier.match(verifier[0])) { - return verifier[1]; - } - - return discriminator; - }, fail); + return url; } diff --git a/src/parser/types.ts b/src/parser/types.ts index f629b90..f4366e5 100644 --- a/src/parser/types.ts +++ b/src/parser/types.ts @@ -1,56 +1,52 @@ export interface Arguments { - vias: string[]; - // Schemeless http identifier - client?: string; - // Indicates whether a room exists on a federating server (assumed to be the - // default), or if the client must connect via the server identified by the - // room ID or event ID - federated?: boolean; - // MXID - sharer?: string; + vias: string[]; + // Schemeless http identifier + client?: string; + // MXID + sharer?: string; } export interface LinkContent { - identifier: string; - arguments: Arguments; - originalLink: string; + identifier: string; + arguments: Arguments; + originalLink: string; } -export enum LinkDiscriminator { - Alias = "ALIAS", - RoomId = "ROOM_ID", - UserId = "USER_ID", - Permalink = "PERMALINK", - GroupId = "GROUP_ID", - ParseFailed = "PARSE_FAILED", +export enum LinkKind { + Alias = "ALIAS", + RoomId = "ROOM_ID", + UserId = "USER_ID", + Permalink = "PERMALINK", + GroupId = "GROUP_ID", + ParseFailed = "PARSE_FAILED", } export interface Alias extends LinkContent { - kind: LinkDiscriminator.Alias; + kind: LinkKind.Alias; } export interface RoomId extends LinkContent { - kind: LinkDiscriminator.RoomId; + kind: LinkKind.RoomId; } export interface UserId extends LinkContent { - kind: LinkDiscriminator.UserId; + kind: LinkKind.UserId; } export interface GroupId extends LinkContent { - kind: LinkDiscriminator.GroupId; + kind: LinkKind.GroupId; } export interface Permalink extends LinkContent { - kind: LinkDiscriminator.Permalink; - roomKind: LinkDiscriminator.RoomId | LinkDiscriminator.Alias; - roomLink: string; - eventId: string; + kind: LinkKind.Permalink; + roomKind: LinkKind.RoomId | LinkKind.Alias; + roomLink: string; + eventId: string; } export interface ParseFailed { - kind: LinkDiscriminator.ParseFailed; - originalLink: string; + kind: LinkKind.ParseFailed; + originalLink: string; } export type SafeLink = Alias | RoomId | UserId | Permalink | GroupId;