Create link parser and formatter
This commit is contained in:
parent
8b07e64a9b
commit
6325ea8328
82
src/parser/parser.test.ts
Normal file
82
src/parser/parser.test.ts
Normal file
@ -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);
|
||||||
|
}
|
||||||
|
});
|
175
src/parser/parser.ts
Normal file
175
src/parser/parser.ts
Normal file
@ -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<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;
|
||||||
|
}
|
||||||
|
}
|
55
src/parser/types.ts
Normal file
55
src/parser/types.ts
Normal file
@ -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;
|
Loading…
Reference in New Issue
Block a user