112 lines
5 KiB
TypeScript
112 lines
5 KiB
TypeScript
import { getCollection, getEntry } from "astro:content";
|
|
import type { Lang, PostWebsite, UserWebsite } from "../content/config";
|
|
import { getWebsiteLinkForUser } from "./get_website_link_for_user";
|
|
import { getUsernameForLang } from "./get_username_for_lang";
|
|
|
|
type MatchGroups =
|
|
| {
|
|
text: string;
|
|
link: string;
|
|
}
|
|
| {
|
|
text: string;
|
|
link: string;
|
|
contentPrefix: "stories" | "games" | "users";
|
|
slug: string;
|
|
};
|
|
|
|
const LOCAL_URL_REGEX =
|
|
/\[(?<text>[^\]]*)\]\((?<link>(?:\/(?<contentPrefix>stories|games|users)(?!\/?\)|\/[1-9]\d*\/?\))\/(?<slug>[^\)]+))|(?:\/[^\)]+?))\/?\)/g;
|
|
|
|
const SERIES_URLS_REGEX_PROMISE = getCollection("series").then(
|
|
(series) => new RegExp(`^(${series.map(({ data }) => data.link.replace(/\/$/, "")).join("|")})\/?$`),
|
|
);
|
|
|
|
/**
|
|
* Given a Markdown text, finds any local URLs (eg. `[click me](/stories/foo)`) and replaces them with the appropriate
|
|
* fully qualified link (eg. `[click me](https://example.com/stories/foo)`).
|
|
* @param originalText The Markdown text to modify.
|
|
* @param lang The language for translating usernames.
|
|
* @param site The base URL of the gallery, to append to the start of local links and permalinks.
|
|
* If undefined, this function will only replace `/users/...` links, while still validating the others.
|
|
* @param website Which website the Markdown text will be rendered to. If defined, any content links
|
|
* (stories, games, etc.) will be replaced by the appropriate link on that website whenever possible.
|
|
* If undefined, this function will only replace `/users/...` links (and permalinks if `site` is set),
|
|
* while still validating the others.
|
|
*/
|
|
export async function qualifyLocalURLsInMarkdown(originalText: string, lang: Lang, site?: URL, website?: PostWebsite) {
|
|
const replacements: string[] = [];
|
|
for (const match of originalText.matchAll(new RegExp(LOCAL_URL_REGEX))) {
|
|
const groups = match.groups as MatchGroups | undefined;
|
|
if (groups?.text === undefined || groups?.link === undefined) {
|
|
throw new Error(`Cannot qualify invalid local URL ${match[0]}`);
|
|
}
|
|
const { text, link } = groups;
|
|
// Any links that match a series `link` are handled separately.
|
|
if ((await SERIES_URLS_REGEX_PROMISE).test(link)) {
|
|
replacements.push(site ? `[${text}](${new URL(link, site).toString()})` : match[0]);
|
|
continue;
|
|
}
|
|
// Check if this is a special link (story, game, or user)
|
|
if ("contentPrefix" in groups && groups.contentPrefix) {
|
|
const { contentPrefix, slug } = groups;
|
|
switch (contentPrefix) {
|
|
case "stories":
|
|
const story = await getEntry("stories", slug);
|
|
if (!story) {
|
|
throw new Error(`Couldn't find story with slug "${slug}"`);
|
|
}
|
|
if (typeof website === "string" && story.data.posts[website as PostWebsite]?.link) {
|
|
replacements.push(`[${text}](${story.data.posts[website as PostWebsite]!.link})`);
|
|
continue;
|
|
}
|
|
break;
|
|
case "games":
|
|
const game = await getEntry("games", slug);
|
|
if (!game) {
|
|
throw new Error(`Couldn't find game with slug "${slug}"`);
|
|
}
|
|
if (typeof website === "string" && game.data.posts[website as PostWebsite]?.link) {
|
|
replacements.push(`[${text}](${game.data.posts[website as PostWebsite]!.link})`);
|
|
continue;
|
|
}
|
|
break;
|
|
case "users":
|
|
const user = await getEntry("users", slug);
|
|
if (!user) {
|
|
throw new Error(`Couldn't find user with id "${slug}"`);
|
|
}
|
|
// If there's a label in the link, use that if possible
|
|
if (text) {
|
|
if (typeof website === "string" && user.data.links[website as UserWebsite]?.link) {
|
|
replacements.push(`[${text}](${user.data.links[website as UserWebsite]!.link})`);
|
|
continue;
|
|
} else if (user.data.preferredLink) {
|
|
replacements.push(`[${text}](${user.data.links[user.data.preferredLink]!.link})`);
|
|
continue;
|
|
}
|
|
}
|
|
// Otherwise (i.e. label is empty), use the username for the website
|
|
replacements.push(
|
|
getWebsiteLinkForUser(user, website as UserWebsite, (user) => getUsernameForLang(user, lang)),
|
|
);
|
|
continue;
|
|
default:
|
|
const unknown: never = contentPrefix;
|
|
console.warn(`WARNING: Unknown local link prefix ${unknown}; ignoring...`);
|
|
}
|
|
}
|
|
replacements.push(website && site ? `[${text}](${new URL(link, site).toString()})` : match[0]);
|
|
}
|
|
const newText = originalText.replaceAll(new RegExp(LOCAL_URL_REGEX), () => {
|
|
const replacement = replacements.shift();
|
|
if (!replacement) {
|
|
throw new Error(`Replacements array length didn't match length of regex matches! (too few elements)`);
|
|
}
|
|
return replacement;
|
|
});
|
|
if (replacements.length > 0) {
|
|
throw new Error(`Replacements array length didn't match length of regex matches! (too many elements)`);
|
|
}
|
|
return newText;
|
|
}
|