personal-site/utils/page_updater/update_pagelist.ts
mrrpnya edbce48613
Some checks failed
Publish to OCI / publish (push) Has been cancelled
beginning i18n support
2025-02-16 19:31:39 -08:00

281 lines
No EOL
9 KiB
TypeScript

import * as pages from "./pages.ts";
import { format } from "date-fns";
/**
* Interface representing a page's essential information.
*/
export interface Page {
/** Metadata for the page (e.g., title, date, etc.) */
metadata: pages.PageInfoMetdata;
/** The page's identifier, e.g. "en/page.md" */
id: string;
/** The URL for the page (relative to content) */
url: string;
/** A hash representing the page's content/version */
hash: string;
}
/**
* Interface representing a category that groups multiple pages.
*/
export interface PageCategory {
/** An array of pages that belong to this category */
posts: Page[];
/** The title of the category */
title: string;
/** A description for the category */
description: string;
/** Tags associated with the category */
tags: string[];
show: boolean;
}
/**
* Class representing a collection of pages across multiple languages.
*
* The structure of a PageList is as follows:
* - last_generated: A timestamp for when the list was last updated.
* - languages: An object mapping language codes (e.g., "en", "es") to objects
* containing categories, which in turn map to PageCategory objects.
*/
export class PageList {
last_generated: string;
languages: {
[lang: string]: {
categories: Record<string, PageCategory>;
};
};
constructor() {
this.last_generated = format(new Date(), "yyyy-MM-dd HH:mm:ss");
this.languages = {};
}
/**
* Deserialize a JSON string to a PageList instance.
* @param json - JSON string representation of a PageList
* @returns A new PageList instance
*/
static fromJSON(json: string): PageList {
return Object.assign(new PageList(), JSON.parse(json));
}
/**
* Custom JSON serialization.
* This method returns a plain object representation, avoiding recursive calls.
* @returns A plain object representation of the PageList.
*/
toJSON(): object {
return {
last_generated: this.last_generated,
languages: this.languages,
};
}
/**
* Returns the JSON string representation of the PageList.
* @returns JSON string of the PageList.
*/
toJSONString(): string {
return JSON.stringify(this, null, 2);
}
/**
* Retrieve a specific category for a given language.
* @param lang - The language code (e.g., "en")
* @param categoryName - The name of the category
* @returns The PageCategory if it exists; otherwise, undefined.
*/
getCategory(lang: string, categoryName: string): PageCategory | undefined {
return this.languages[lang]?.categories[categoryName];
}
/**
* Retrieve all pages for a given language and category.
* @param lang - The language code (e.g., "en")
* @param categoryName - The name of the category
* @returns An array of pages, or an empty array if not found.
*/
getPagesByCategory(lang: string, categoryName: string): Page[] {
return this.languages[lang]?.categories[categoryName]?.posts || [];
}
/**
* Compute the canonical ID for a page by removing the language prefix.
* For example, "en/page.md" becomes "page.md".
* @param pageId - The full page id including language folder
* @returns The canonical page id
*/
static getCanonicalId(pageId: string): string {
return pageId.replace(/^[^/]+\//, "");
}
/**
* Retrieve a specific page by its canonical ID from a given language and category.
* @param lang - The language code (e.g., "en")
* @param categoryName - The category containing the page
* @param canonicalId - The canonical id of the page (language prefix removed)
* @returns The Page if found; otherwise, undefined.
*/
getPage(
lang: string,
categoryName: string,
canonicalId: string,
): Page | undefined {
const pagesArr = this.getPagesByCategory(lang, categoryName);
return pagesArr.find((page) =>
PageList.getCanonicalId(page.id) === canonicalId
);
}
/**
* Identify all languages in which a page (by its canonical ID) is available.
* @param canonicalId - The canonical id of the page (language prefix removed)
* @returns An array of language codes where the page exists.
*/
getAvailableLanguagesForPage(canonicalId: string): string[] {
const langs: string[] = [];
for (const lang of Object.keys(this.languages)) {
const categories = this.languages[lang].categories;
for (const categoryName of Object.keys(categories)) {
if (
categories[categoryName].posts.some((page) =>
PageList.getCanonicalId(page.id) === canonicalId
)
) {
langs.push(lang);
break; // Page found in this language; no need to check further categories.
}
}
}
return langs;
}
/**
* Enumerate unique pages across all languages.
* Each unique page (by canonical id) is listed along with:
* - An array of languages in which it is available.
* - A mapping of each language to its corresponding Page.
* @returns An array of objects representing unique pages.
*/
enumerateUniquePages(): Array<
{
canonicalId: string;
langs: string[];
pages: { [lang: string]: Page };
}
> {
const uniquePages: {
[canonicalId: string]: {
langs: Set<string>;
pages: { [lang: string]: Page };
};
} = {};
for (const lang of Object.keys(this.languages)) {
const categories = this.languages[lang].categories;
for (const categoryName of Object.keys(categories)) {
for (const page of categories[categoryName].posts) {
const canonicalId = PageList.getCanonicalId(page.id);
if (!uniquePages[canonicalId]) {
uniquePages[canonicalId] = {
langs: new Set(),
pages: {},
};
}
uniquePages[canonicalId].langs.add(lang);
uniquePages[canonicalId].pages[lang] = page;
}
}
}
return Object.entries(uniquePages).map(([canonicalId, data]) => ({
canonicalId,
langs: Array.from(data.langs),
pages: data.pages,
}));
}
}
/**
* Generate a PageCategory from the provided pagesInfo object.
*
* @param pagesInfo - An object where keys are paths (e.g., "content/[lang]/...") and values are page data.
* @returns A PageCategory object containing an array of Page objects.
*/
export function generatePageCategory(pagesInfo: Record<string, any>): PageCategory {
const pageList: Page[] = [];
for (const [filePath, page] of Object.entries(pagesInfo)) {
// Expect the path in the form "content/[lang]/..."
const langMatch = filePath.match(/^content\/([^/]+)\//);
const lang = langMatch ? langMatch[1] : "default";
const pageDict: Page = {
metadata: page.metadata,
id: page.local_path, // e.g. "en/page.md"
url: page.absolute_path.replace("content/", ""), // Remove "content/" prefix
hash: page.hash,
};
pageList.push(pageDict);
}
// Format dates consistently.
pageList.forEach((page) => {
if (page.metadata.date) {
page.metadata.date = format(
new Date(page.metadata.date),
"yyyy-MM-dd HH:mm:ss",
);
}
});
return {
posts: pageList,
title: "",
description: "",
tags: [],
show: true
};
}
// ---------------------------------------------------------------------------
// Directory Definitions
// ---------------------------------------------------------------------------
/**
* An array of directories to scan for pages.
* Each directory is represented as a PageLocation from the `pages` module.
*
* Note: The 'map' property should correspond to the subdirectory name that
* exists within each language folder. For example, if articles for "Site"
* are stored in "content/en/site", "content/fr/site", etc., then map: "site".
*/
export const postDirectories: pages.PageLocation[] = [
{
title: "Site",
description: "Articles to test site functionality",
tags: ["site"],
map: "site",
root: "", // Not used in the new structure
show: false
},
{
title: "Collections",
description:
"Articles that are collections of information: Lists, Awesome lists, etc.",
tags: ["collection"],
map: "collections",
root: "",
show: true
},
{
title: "Guides",
description: "Guides and tutorials",
tags: ["guide"],
map: "guides",
root: "",
show: true
},
];