Improve i18n support and config validation

This commit is contained in:
Bad Manners 2024-03-30 15:04:29 -03:00
parent 4f83ae8802
commit 37db38b613
9 changed files with 152 additions and 152 deletions

4
package-lock.json generated
View file

@ -1,12 +1,12 @@
{ {
"name": "gallery-badmanners-xyz", "name": "gallery-badmanners-xyz",
"version": "1.2.0", "version": "1.2.1",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "gallery-badmanners-xyz", "name": "gallery-badmanners-xyz",
"version": "1.2.0", "version": "1.2.1",
"dependencies": { "dependencies": {
"@astrojs/check": "^0.5.9", "@astrojs/check": "^0.5.9",
"@astrojs/rss": "^4.0.5", "@astrojs/rss": "^4.0.5",

View file

@ -1,7 +1,7 @@
{ {
"name": "gallery-badmanners-xyz", "name": "gallery-badmanners-xyz",
"type": "module", "type": "module",
"version": "1.2.0", "version": "1.2.1",
"scripts": { "scripts": {
"dev": "astro dev", "dev": "astro dev",
"start": "astro dev", "start": "astro dev",

View file

@ -15,15 +15,11 @@ if (user.data.isAnonymous) {
const username = t(lang, user.data.nameLang as any) || user.data.name; const username = t(lang, user.data.nameLang as any) || user.data.name;
let link: string | null = null; let link: string | null = null;
if (user.data.preferredLink) { if (user.data.preferredLink) {
if (user.data.preferredLink in user.data.links) { const preferredLink = user.data.links[user.data.preferredLink] as string | [string, string];
const preferredLink = user.data.links[user.data.preferredLink] as string | [string, string]; if (typeof preferredLink === "string") {
if (typeof preferredLink === "string") { link = preferredLink;
link = preferredLink;
} else {
link = preferredLink[0];
}
} else { } else {
throw new Error(`No preferredLink "${user.data.preferredLink}" for user ${user.id}`); link = preferredLink[0];
} }
} }
--- ---

View file

@ -17,6 +17,8 @@ export const WEBSITE_LIST = [
] as const; ] as const;
const localUrlRegex = /^((\/?\.\.(?=\/))+|(\.(?=\/)))?\/?[a-z0-9_-]+(\/[a-z0-9_-]+)*\/?$/; const localUrlRegex = /^((\/?\.\.(?=\/))+|(\.(?=\/)))?\/?[a-z0-9_-]+(\/[a-z0-9_-]+)*\/?$/;
const refineAuthors = (value: { id: any } | any[]) => "id" in value || value.length > 0;
const refineCopyrightedCharacters = (value: Record<string, any>) => !("" in value) || Object.keys(value).length == 1;
const lang = z.enum(["eng", "tok"]).default("eng"); const lang = z.enum(["eng", "tok"]).default("eng");
const website = z.enum(WEBSITE_LIST); const website = z.enum(WEBSITE_LIST);
@ -43,7 +45,10 @@ const storiesCollection = defineCollection({
// Optional // Optional
isDraft: z.boolean().default(false), isDraft: z.boolean().default(false),
shortTitle: z.string().optional(), shortTitle: z.string().optional(),
authors: z.union([reference("users"), z.array(reference("users"))]).default("bad-manners"), authors: z
.union([reference("users"), z.array(reference("users"))])
.default("bad-manners")
.refine(refineAuthors, "authors cannot be empty"),
descriptionPlaintext: z.string().optional(), descriptionPlaintext: z.string().optional(),
summary: z.string().optional(), summary: z.string().optional(),
thumbnail: image().optional(), thumbnail: image().optional(),
@ -52,7 +57,13 @@ const storiesCollection = defineCollection({
series: reference("series").optional(), series: reference("series").optional(),
commissioner: reference("users").optional(), commissioner: reference("users").optional(),
requester: reference("users").optional(), requester: reference("users").optional(),
copyrightedCharacters: z.record(z.string(), reference("users")).default({}), copyrightedCharacters: z
.record(z.string(), reference("users"))
.default({})
.refine(
refineCopyrightedCharacters,
"copyrightedCharacters cannot have an empty catch-all key with other keys",
),
lang, lang,
prev: reference("stories").nullish(), prev: reference("stories").nullish(),
next: reference("stories").nullish(), next: reference("stories").nullish(),
@ -74,13 +85,22 @@ const gamesCollection = defineCollection({
tags: z.array(z.string()), tags: z.array(z.string()),
// Optional // Optional
isDraft: z.boolean().default(false), isDraft: z.boolean().default(false),
authors: z.union([reference("users"), z.array(reference("users"))]).default("bad-manners"), authors: z
.union([reference("users"), z.array(reference("users"))])
.default("bad-manners")
.refine(refineAuthors, "authors cannot be empty"),
descriptionPlaintext: z.string().optional(), descriptionPlaintext: z.string().optional(),
thumbnail: image().optional(), thumbnail: image().optional(),
thumbnailWidth: z.number().int().optional(), thumbnailWidth: z.number().int().optional(),
thumbnailHeight: z.number().int().optional(), thumbnailHeight: z.number().int().optional(),
series: reference("series").optional(), series: reference("series").optional(),
copyrightedCharacters: z.record(z.string(), reference("users")).default({}), copyrightedCharacters: z
.record(z.string(), reference("users"))
.default({})
.refine(
refineCopyrightedCharacters,
"copyrightedCharacters cannot have an empty catch-all key with other keys",
),
lang, lang,
relatedStories: z.array(reference("stories")).default([]), relatedStories: z.array(reference("stories")).default([]),
relatedGames: z.array(reference("games")).default([]), relatedGames: z.array(reference("games")).default([]),
@ -91,16 +111,24 @@ const gamesCollection = defineCollection({
const usersCollection = defineCollection({ const usersCollection = defineCollection({
type: "data", type: "data",
schema: ({ image }) => schema: ({ image }) =>
z.object({ z
// Required .object({
name: z.string(), // Required
links: z.record(website, z.union([z.string().url(), z.tuple([z.string().url(), z.string()])])), name: z.string(),
// Optional links: z.record(website, z.union([z.string().url(), z.tuple([z.string().url(), z.string()])])),
preferredLink: website.nullish(), // Optional
nameLang: z.record(lang, z.string()).default({}), preferredLink: website.nullish(),
avatar: image().optional(), nameLang: z.record(lang, z.string()).default({}),
isAnonymous: z.boolean().default(false), avatar: image().optional(),
}), isAnonymous: z.boolean().default(false),
})
.refine(
({ links, preferredLink }) => !preferredLink || preferredLink in links,
({ preferredLink }) => ({
message: `"${preferredLink}" not defined in links`,
path: ["preferredLink"],
}),
),
}); });
const seriesCollection = defineCollection({ const seriesCollection = defineCollection({
@ -118,7 +146,12 @@ const tagCategoriesCollection = defineCollection({
// Required // Required
name: z.string(), name: z.string(),
index: z.number().int(), index: z.number().int(),
tags: z.array(z.union([z.string(), z.record(lang, z.string())])), tags: z.array(
z.union([
z.string(),
z.record(lang, z.string()).refine((tag) => "eng" in tag, 'Object-formatted tag must have an "eng" key'),
]),
),
}), }),
}); });

View file

@ -7,6 +7,29 @@ export type TranslationRecord = { [DEFAULT_LANG]: string | ((...args: any[]) =>
}; };
export const UI_STRINGS: Record<string, TranslationRecord> = { export const UI_STRINGS: Record<string, TranslationRecord> = {
"util/join_names": {
eng: (names: string[]) =>
names.length <= 1
? names.join("")
: names.length == 2
? `${names[0]} and ${names[1]}`
: `${names.slice(0, -1).join(", ")}, and ${names[names.length - 1]}`,
tok: (names: string[]) => names.join(" en "),
},
"export_story/warnings": {
eng: (wordCount: number | string, contentWarning: string) => `*Word count: ${wordCount}. ${contentWarning}*`,
tok: (_wordCount: number | string, contentWarning: string) => `*${contentWarning}*`,
},
"export_story/writing": {
eng: (authorsList: string[]) => `Writing: ${authorsList.join(" ")}`,
tok: (authorsList: string[]) => `lipu ni li tan jan ni: ${authorsList.join(" en ")}`,
},
"export_story/request_for": {
eng: (requester: string) => `Request for: ${requester}`,
},
"export_story/commissioned_by": {
eng: (commissioner: string) => `Commissioned by: ${commissioner}`,
},
"story/return_to_stories": { "story/return_to_stories": {
eng: "Return to stories", eng: "Return to stories",
tok: "o tawa e lipu ale", tok: "o tawa e lipu ale",
@ -59,24 +82,12 @@ export const UI_STRINGS: Record<string, TranslationRecord> = {
tok: "lipu lawa", tok: "lipu lawa",
}, },
"story/authors": { "story/authors": {
eng: (authorsList: string[]) => { eng: (authorsList: string[]) =>
let authorsString = `by ${authorsList[0]}`; `by ${(UI_STRINGS["util/join_names"]!.eng as (arg: string[]) => string)(authorsList)}`,
if (authorsList.length > 2) { tok: (authorsList: string[]) =>
authorsString += `, ${authorsList.slice(1, authorsList.length - 1).join(", ")}, and ${authorsList[authorsList.length - 1]}`; authorsList.length > 1
} else if (authorsList.length == 2) { ? `lipu ni li tan jan ni: ${(UI_STRINGS["util/join_names"]!.tok as (arg: string[]) => string)(authorsList)}`
authorsString += ` and ${authorsList[1]}`; : `lipu ni li tan ${authorsList[0]}`,
}
return authorsString;
},
tok: (authorsList: string[]) => {
let authorsString = "lipu ni li tan ";
if (authorsList.length > 1) {
authorsString += `jan ni: ${authorsList.join(" en ")}`;
} else {
authorsString += authorsList[0];
}
return authorsString;
},
}, },
"story/commissioned_by": { "story/commissioned_by": {
eng: (arg: string) => `Commissioned by ${arg}`, eng: (arg: string) => `Commissioned by ${arg}`,
@ -85,15 +96,10 @@ export const UI_STRINGS: Record<string, TranslationRecord> = {
eng: (arg: string) => `Requested by ${arg}`, eng: (arg: string) => `Requested by ${arg}`,
}, },
"characters/characters_are_copyrighted_by": { "characters/characters_are_copyrighted_by": {
eng: (owner: string, charactersList: string[]) => { eng: (owner: string, charactersList: string[]) =>
if (charactersList.length == 1) { charactersList.length == 1
return `${charactersList[0]} is © ${owner}`; ? `${charactersList[0]} is © ${owner}`
} : `${(UI_STRINGS["util/join_names"]!["eng"] as (arg: string[]) => string)(charactersList)} are © ${owner}`,
if (charactersList.length == 2) {
return `${charactersList[0]} and ${charactersList[1]} are © ${owner}`;
}
return `${charactersList.slice(0, -1).join(", ")}, and ${charactersList[charactersList.length - 1]} are © ${owner}`;
},
}, },
"characters/all_characters_are_copyrighted_by": { "characters/all_characters_are_copyrighted_by": {
eng: (owner: string) => `All characters are © ${owner}`, eng: (owner: string) => `All characters are © ${owner}`,

View file

@ -16,9 +16,6 @@ type Props = CollectionEntry<"games">["data"];
const { props } = Astro; const { props } = Astro;
const series = props.series && (await getEntry(props.series)); const series = props.series && (await getEntry(props.series));
const authors = await getEntries([props.authors].flat()); const authors = await getEntries([props.authors].flat());
if ("" in props.copyrightedCharacters && Object.keys(props.copyrightedCharacters).length > 1) {
throw new Error("copyrightedCharacter cannot use empty key (catch-all) with other keys");
}
const copyrightedCharacters = await Promise.all( const copyrightedCharacters = await Promise.all(
Object.values( Object.values(
Object.keys(props.copyrightedCharacters).reduce( Object.keys(props.copyrightedCharacters).reduce(

View file

@ -26,9 +26,6 @@ const series = props.series && (await getEntry(props.series));
const authors = await getEntries([props.authors].flat()); const authors = await getEntries([props.authors].flat());
const commissioner = props.commissioner && (await getEntry(props.commissioner)); const commissioner = props.commissioner && (await getEntry(props.commissioner));
const requester = props.requester && (await getEntry(props.requester)); const requester = props.requester && (await getEntry(props.requester));
if ("" in props.copyrightedCharacters && Object.keys(props.copyrightedCharacters).length > 1) {
throw new Error("copyrightedCharacter cannot use empty key (catch-all) with other keys");
}
const copyrightedCharacters = await Promise.all( const copyrightedCharacters = await Promise.all(
Object.values( Object.values(
Object.keys(props.copyrightedCharacters).reduce( Object.keys(props.copyrightedCharacters).reduce(

View file

@ -5,7 +5,7 @@ import { decode as tinyDecode } from "tiny-decode";
import { type Lang, type Website } from "../../../content/config"; import { type Lang, type Website } from "../../../content/config";
import { t } from "../../../i18n"; import { t } from "../../../i18n";
type DescriptionFormat = "bbcode" | "markdown"; type ExportFormat = "bbcode" | "markdown";
const WEBSITE_LIST = [ const WEBSITE_LIST = [
["eka", "bbcode"], ["eka", "bbcode"],
@ -13,9 +13,9 @@ const WEBSITE_LIST = [
["inkbunny", "bbcode"], ["inkbunny", "bbcode"],
["sofurry", "bbcode"], ["sofurry", "bbcode"],
["weasyl", "markdown"], ["weasyl", "markdown"],
] as const satisfies [Website, DescriptionFormat][]; ] as const satisfies [Website, ExportFormat][];
type ExportWebsite = typeof WEBSITE_LIST extends ReadonlyArray<[infer K, DescriptionFormat]> ? K : never; type ExportWebsite = typeof WEBSITE_LIST extends ReadonlyArray<[infer K, ExportFormat]> ? K : never;
const bbcodeRenderer: RendererApi = { const bbcodeRenderer: RendererApi = {
strong: (text) => `[b]${text}[/b]`, strong: (text) => `[b]${text}[/b]`,
@ -108,7 +108,7 @@ function getUsernameForWebsite(user: CollectionEntry<"users">, website: Website)
} }
break; break;
default: default:
throw new Error(`Unhandled website "${website}" in getUsernameForWebsite`); throw new Error(`Unhandled Website "${website}"`);
} }
} else { } else {
return link[1].replace(/^@/, ""); return link[1].replace(/^@/, "");
@ -117,24 +117,14 @@ function getUsernameForWebsite(user: CollectionEntry<"users">, website: Website)
throw new Error(`Cannot get "${website}" username for user "${user.id}"`); throw new Error(`Cannot get "${website}" username for user "${user.id}"`);
} }
function isPreferredWebsite( function isPreferredWebsite(user: CollectionEntry<"users">, website: Website): boolean {
user: CollectionEntry<"users">, const { preferredLink } = user.data;
website: Website, return !preferredLink || preferredLink == website;
preferredChoices: readonly Website[],
): boolean {
const { preferredLink, links } = user.data;
if (!(website in links)) {
return false;
}
if (!preferredLink || preferredLink == website) {
return true;
}
return !preferredChoices.includes(preferredLink);
} }
function getLinkForUser(user: CollectionEntry<"users">, website: ExportWebsite): string { function getLinkForUser(user: CollectionEntry<"users">, website: ExportWebsite, anonymousFallback: string): string {
if (user.data.isAnonymous) { if (user.data.isAnonymous) {
return "anonymous"; return anonymousFallback;
} }
switch (website) { switch (website) {
case "eka": case "eka":
@ -148,51 +138,46 @@ function getLinkForUser(user: CollectionEntry<"users">, website: ExportWebsite):
} }
break; break;
case "weasyl": case "weasyl":
const weasylPreferredWebsites = ["furaffinity", "inkbunny", "sofurry"] as const;
if ("weasyl" in user.data.links) { if ("weasyl" in user.data.links) {
return `<!~${getUsernameForWebsite(user, "weasyl").replaceAll(" ", "")}>`; return `<!~${getUsernameForWebsite(user, "weasyl").replaceAll(" ", "")}>`;
} else if (isPreferredWebsite(user, "furaffinity", weasylPreferredWebsites)) { } else if (isPreferredWebsite(user, "furaffinity")) {
return `<fa:${getUsernameForWebsite(user, "furaffinity")}>`; return `<fa:${getUsernameForWebsite(user, "furaffinity")}>`;
} else if (isPreferredWebsite(user, "inkbunny", weasylPreferredWebsites)) { } else if (isPreferredWebsite(user, "inkbunny")) {
return `<ib:${getUsernameForWebsite(user, "inkbunny")}>`; return `<ib:${getUsernameForWebsite(user, "inkbunny")}>`;
} else if (isPreferredWebsite(user, "sofurry", weasylPreferredWebsites)) { } else if (isPreferredWebsite(user, "sofurry")) {
return `<sf:${getUsernameForWebsite(user, "sofurry")}>`; return `<sf:${getUsernameForWebsite(user, "sofurry")}>`;
} }
break; break;
case "inkbunny": case "inkbunny":
const inkbunnyPreferredWebsites = ["furaffinity", "sofurry", "weasyl"] as const;
if ("inkbunny" in user.data.links) { if ("inkbunny" in user.data.links) {
return `[iconname]${getUsernameForWebsite(user, "inkbunny")}[/iconname]`; return `[iconname]${getUsernameForWebsite(user, "inkbunny")}[/iconname]`;
} else if (isPreferredWebsite(user, "furaffinity", inkbunnyPreferredWebsites)) { } else if (isPreferredWebsite(user, "furaffinity")) {
return `[fa]${getUsernameForWebsite(user, "furaffinity")}[/fa]`; return `[fa]${getUsernameForWebsite(user, "furaffinity")}[/fa]`;
} else if (isPreferredWebsite(user, "sofurry", inkbunnyPreferredWebsites)) { } else if (isPreferredWebsite(user, "sofurry")) {
return `[sf]${getUsernameForWebsite(user, "sofurry")}[/sf]`; return `[sf]${getUsernameForWebsite(user, "sofurry")}[/sf]`;
} else if (isPreferredWebsite(user, "weasyl", inkbunnyPreferredWebsites)) { } else if (isPreferredWebsite(user, "weasyl")) {
return `[weasyl]${getUsernameForWebsite(user, "weasyl")}[/weasyl]`; return `[weasyl]${getUsernameForWebsite(user, "weasyl")}[/weasyl]`;
} }
break; break;
case "sofurry": case "sofurry":
const sofurryPreferredWebsites = ["furaffinity", "inkbunny"] as const;
if ("sofurry" in user.data.links) { if ("sofurry" in user.data.links) {
return `:icon${getUsernameForWebsite(user, "sofurry")}:`; return `:icon${getUsernameForWebsite(user, "sofurry")}:`;
} else if (isPreferredWebsite(user, "furaffinity", sofurryPreferredWebsites)) { } else if (isPreferredWebsite(user, "furaffinity")) {
return `fa!${getUsernameForWebsite(user, "furaffinity")}`; return `fa!${getUsernameForWebsite(user, "furaffinity")}`;
} else if (isPreferredWebsite(user, "inkbunny", sofurryPreferredWebsites)) { } else if (isPreferredWebsite(user, "inkbunny")) {
return `ib!${getUsernameForWebsite(user, "inkbunny")}`; return `ib!${getUsernameForWebsite(user, "inkbunny")}`;
} }
break; break;
default: default:
throw new Error(`Unhandled website "${website}" in getLinkForUser`); throw new Error(`Unhandled ExportWebsite "${website}"`);
} }
if (user.data.preferredLink) { if (user.data.preferredLink) {
if (user.data.preferredLink in user.data.links) { const preferredLink = user.data.links[user.data.preferredLink] as string | [string, string];
const preferredLink = user.data.links[user.data.preferredLink] as string | [string, string]; return `[${user.data.name}](${typeof preferredLink === "string" ? preferredLink : preferredLink[0]})`;
return `[${user.data.name}](${typeof preferredLink === "string" ? preferredLink : preferredLink[0]})`;
} else {
throw new Error(`No preferredLink "${user.data.preferredLink}" for user ${user.id}`);
}
} }
throw new Error(`No "${website}" link for user "${user.id}" (consider setting preferredLink)`); throw new Error(
`No matching "${website}" link for user "${user.id}" (consider setting their "preferredLink" property)`,
);
} }
function getNameForUser(user: CollectionEntry<"users">, anonymousUser: CollectionEntry<"users">, lang: Lang): string { function getNameForUser(user: CollectionEntry<"users">, anonymousUser: CollectionEntry<"users">, lang: Lang): string {
@ -222,55 +207,47 @@ export const getStaticPaths: GetStaticPaths = async () => {
export const GET: APIRoute<Props, Params> = async ({ props: { story }, site }) => { export const GET: APIRoute<Props, Params> = async ({ props: { story }, site }) => {
const { lang } = story.data; const { lang } = story.data;
if ( const copyrightedCharacters = await Promise.all(
story.data.copyrightedCharacters && Object.values(
"" in story.data.copyrightedCharacters && Object.keys(story.data.copyrightedCharacters).reduce(
Object.keys(story.data.copyrightedCharacters).length > 1 (acc, character) => {
) { const user = story.data.copyrightedCharacters[character];
throw new Error("copyrightedCharacter cannot use empty key (catch-all) with other keys"); if (!(user.id in acc)) {
} acc[user.id] = [getEntry(user), []];
const charactersPerUser = }
story.data.copyrightedCharacters && acc[user.id][1].push(character);
Object.keys(story.data.copyrightedCharacters).reduce( return acc;
(acc, character) => { },
const key = story.data.copyrightedCharacters[character].id; {} as Record<string, [Promise<CollectionEntry<"users">>, string[]]>,
if (!(key in acc)) { ),
acc[key] = []; ).map(async ([userPromise, characters]) => [await userPromise, characters] as [CollectionEntry<"users">, string[]]),
} );
acc[key].push(character); const authorsList = await getEntries([story.data.authors].flat());
return acc; const commissioner = story.data.commissioner && (await getEntry(story.data.commissioner));
}, const requester = story.data.requester && (await getEntry(story.data.requester));
{} as Record< const anonymousUser = await getEntry("users", "anonymous");
CollectionEntry<"users">["id"], const anonymousFallback = getNameForUser(anonymousUser, anonymousUser, lang);
(typeof story.data.copyrightedCharacters extends Record<infer K, any> ? K : never)[]
>,
);
const description: Record<ExportWebsite, string> = Object.fromEntries( const description: Record<ExportWebsite, string> = Object.fromEntries(
await Promise.all( await Promise.all(
WEBSITE_LIST.map(async ([website, exportFormat]) => { WEBSITE_LIST.map(async ([website, exportFormat]) => {
const u = (user: CollectionEntry<"users">) => getLinkForUser(user, website); const u = (user: CollectionEntry<"users">) => getLinkForUser(user, website, anonymousFallback);
const storyDescription = ( const storyDescription = (
[ [
story.data.description, story.data.description,
`*Word count: ${story.data.wordCount}. ${story.data.contentWarning.trim()}*`, t(lang, "export_story/warnings", story.data.wordCount, story.data.contentWarning.trim()),
"Writing: " + (await getEntries([story.data.authors].flat())).map((author) => u(author)).join(" "), t(
story.data.requester && "Request for: " + u(await getEntry(story.data.requester)), lang,
story.data.commissioner && "Commissioned by: " + u(await getEntry(story.data.commissioner)), "export_story/writing",
...(await Promise.all( authorsList.map((author) => u(author)),
(Object.keys(charactersPerUser) as CollectionEntry<"users">["id"][]).map(async (id) => { ),
const user = u(await getEntry("users", id)); requester && t(lang, "export_story/request_for", u(requester)),
const characterList = charactersPerUser[id]; commissioner && t(lang, "export_story/commissioned_by", u(commissioner)),
if (characterList[0] == "") { ...copyrightedCharacters.map(([user, characterList]) =>
return `All characters are © ${user}`; characterList[0] == ""
} else if (characterList.length > 2) { ? t(lang, "characters/all_characters_are_copyrighted_by", u(user))
return `${characterList.slice(0, characterList.length - 1).join(", ")}, and ${characterList[characterList.length - 1]} are © ${user}`; : t(lang, "characters/characters_are_copyrighted_by", u(user), characterList),
} else if (characterList.length > 1) { ),
return `${characterList[0]} and ${characterList[1]} are © ${user}`;
}
return `${characterList[0]} is © ${user}`;
}),
)),
].filter((data) => data) as string[] ].filter((data) => data) as string[]
) )
.join("\n\n") .join("\n\n")
@ -290,25 +267,22 @@ export const GET: APIRoute<Props, Params> = async ({ props: { story }, site }) =
if (exportFormat === "markdown") { if (exportFormat === "markdown") {
return [website, storyDescription.replaceAll(/\n\n\n+/g, "\n\n").trim()]; return [website, storyDescription.replaceAll(/\n\n\n+/g, "\n\n").trim()];
} }
throw new Error(`Unknown exportFormat "${exportFormat}"`); throw new Error(`Unhandled ExportFormat "${exportFormat}"`);
}), }),
), ),
); );
const anonymousUser = await getEntry("users", "anonymous");
const authorsNames = (await getEntries([story.data.authors].flat())).map((author) =>
getNameForUser(author, anonymousUser, lang),
);
const commissioner = story.data.commissioner && (await getEntry(story.data.commissioner));
const requester = story.data.requester && (await getEntry(story.data.requester));
const storyHeader = const storyHeader =
`${story.data.title}\n` + `${story.data.title}\n` +
`${t(lang, "story/authors", authorsNames)}\n` + `${t(
lang,
"story/authors",
authorsList.map((author) => getNameForUser(author, anonymousUser, lang)),
)}\n` +
(commissioner ? `${t(lang, "story/commissioned_by", getNameForUser(commissioner, anonymousUser, lang))}\n` : "") + (commissioner ? `${t(lang, "story/commissioned_by", getNameForUser(commissioner, anonymousUser, lang))}\n` : "") +
(requester ? `${t(lang, "story/requested_by", getNameForUser(requester, anonymousUser, lang))}\n` : ""); (requester ? `${t(lang, "story/requested_by", getNameForUser(requester, anonymousUser, lang))}\n` : "");
const storyText = `${storyHeader}\n===\n\n${story.body.replaceAll("\\*", "*").replaceAll("\\=", "=")}` const storyText = `${storyHeader}\n===\n\n${story.body.replaceAll(/\\([=*])/g, "$1")}`
.replaceAll(/\n\n\n+/g, "\n\n") .replaceAll(/\n\n\n+/g, "\n\n")
.trim(); .trim();

View file

@ -52,9 +52,6 @@ const categorizedTags: Array<[string, string, string[]]> = tagCategories
if (typeof tag === "string") { if (typeof tag === "string") {
return tag; return tag;
} }
if (!("eng" in tag)) {
throw new Error(`Object-formatted tag must have an "eng" key: ${tag}`);
}
return tag["eng"]!; return tag["eng"]!;
}); });
tagList.forEach((tag, index) => { tagList.forEach((tag, index) => {