Skip to content
Snippets Groups Projects
Commit 6325ea83 authored by Jorik Schellekens's avatar Jorik Schellekens
Browse files

Create link parser and formatter

parent 8b07e64a
No related branches found
No related tags found
No related merge requests found
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);
}
});
import _ from "lodash";
import {
LinkDiscriminator,
SafeLink,
Link,
LinkContent,
Arguments,
} from "./types";
/*
* Verifiers are regexes which will match valid
* identifiers to their type
*/
type Verifier<A> = [RegExp, A];
export const roomVerifiers: Verifier<
LinkDiscriminator.Alias | LinkDiscriminator.RoomId
>[] = [
[/^#([^\/:]+?):(.+)$/, LinkDiscriminator.Alias],
[/^!([^\/:]+?):(.+)$/, LinkDiscriminator.RoomId],
];
export const verifiers: Verifier<LinkDiscriminator>[] = [
[/^[\!#]([^\/:]+?):(.+?)\/\$([^\/:]+?):(.+?)$/, 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<T, F>(
identifier: string,
verifiers: Verifier<T>[],
fail: F
): T | F {
if (identifier !== encodeURI(identifier)) {
return fail;
}
return verifiers.reduce<T | F>((discriminator, verifier) => {
if (discriminator !== fail) {
return discriminator;
}
if (identifier.match(verifier[0])) {
return verifier[1];
}
return discriminator;
}, fail);
}
/*
* parseArgs parses the <extra args> 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;
}
}
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;
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment