@renyuneyun/parse-link-header-ts
Version:
Parses a link header and returns information for each contained link.
86 lines (66 loc) • 1.88 kB
text/typescript
const PARSE_LINK_HEADER_MAXLEN = 2000;
interface RawLink {
url: string;
[key: string]: string;
}
interface Link extends RawLink {
rel: string;
}
export type Result = Record<string, Link[]>;
function hasRel(link: null | Record<string, string>): link is Link {
if (link === null) {
return false;
}
return link.rel != null;
}
function intoRels(acc: Result, link: Link) {
function splitRel(rel: string) {
if (!(rel in acc)) {
acc[rel] = [];
}
acc[rel].push({ ...link, ...{ rel } });
}
link.rel.split(/\s+/).forEach(splitRel);
return acc;
}
function createObjects(acc: Record<string, string>, p: string) {
// rel="next" => 1: rel 2: next
const m = p.match(/\s*(.+)\s*=\s*"?([^"]+)"?/);
if (m) acc[m[1]] = m[2];
return acc;
}
function parseLink(link: string, base?: string): RawLink | null {
const m = link.match(/<?([^>]*)>(.*)/);
if (m === null) {
throw new Error('Invalid link header');
}
const linkUrl = m[1];
const parts = m[2].split(';');
const parsedUrl = new URL(linkUrl, base);
const qry: Record<string, string> = {};
for (const [key, value] of parsedUrl.searchParams) {
qry[key] = value;
}
parts.shift();
let info = parts.reduce<Record<string, string>>(createObjects, {});
info = { ...qry, ...info, url: linkUrl };
return info as RawLink;
}
function checkHeader(linkHeader: string): void {
if (linkHeader.length > PARSE_LINK_HEADER_MAXLEN) {
throw new Error(
`Input string too long, it should be under ${PARSE_LINK_HEADER_MAXLEN} characters.`
);
}
}
export default function (linkHeader: string, base?: string): Result {
if (linkHeader === '') {
throw new Error('linkHeader is empty');
}
checkHeader(linkHeader);
return linkHeader
.split(/,\s*</)
.map((link) => parseLink(link, base))
.filter<Link>(hasRel)
.reduce(intoRels, {});
}