Add annotation page gallery

This commit is contained in:
wuyang6
2026-07-20 10:56:59 +08:00
parent b7798e9985
commit 7cd6e5e464
5 changed files with 307 additions and 0 deletions
+88
View File
@@ -0,0 +1,88 @@
import { mkdir, open, readdir, stat } from "node:fs/promises";
import path from "node:path";
export type AnnotationPageInfo = {
name: string;
title: string;
size: number;
modifiedAt: string;
href: string;
};
const annotationPagesRoot = path.resolve(
process.env.ANNOTATION_PAGES_DIR?.trim() || "/home/mi/annotation-pages",
);
export async function listAnnotationPages(): Promise<AnnotationPageInfo[]> {
await mkdir(annotationPagesRoot, { recursive: true });
const entries = await readdir(annotationPagesRoot, { withFileTypes: true });
const pages = await Promise.all(
entries
.filter((entry) => entry.isFile() && isSafeHtmlName(entry.name))
.map(async (entry) => {
const filePath = path.join(annotationPagesRoot, entry.name);
const [fileStat, title] = await Promise.all([
stat(filePath),
readHtmlTitle(filePath, entry.name),
]);
return {
name: entry.name,
title,
size: fileStat.size,
modifiedAt: fileStat.mtime.toISOString(),
href: `/annotations/${encodeURIComponent(entry.name)}`,
};
}),
);
return pages.sort(
(left, right) =>
Date.parse(right.modifiedAt) - Date.parse(left.modifiedAt) ||
left.name.localeCompare(right.name, "zh-CN"),
);
}
export function resolveAnnotationPage(name: string): string | null {
if (!isSafeHtmlName(name)) return null;
const resolved = path.resolve(annotationPagesRoot, name);
if (path.dirname(resolved) !== annotationPagesRoot) return null;
return resolved;
}
function isSafeHtmlName(name: string): boolean {
return (
name === path.basename(name) &&
!name.startsWith(".") &&
/\.html?$/i.test(name)
);
}
async function readHtmlTitle(filePath: string, fileName: string) {
const handle = await open(filePath, "r");
try {
const buffer = Buffer.alloc(16 * 1024);
const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0);
const head = buffer.subarray(0, bytesRead).toString("utf8");
const match = head.match(/<title[^>]*>([\s\S]*?)<\/title>/i);
const title = match?.[1]
?.replace(/<[^>]+>/g, " ")
.replace(/\s+/g, " ")
.trim();
return title ? decodeHtmlEntities(title) : stripHtmlExtension(fileName);
} finally {
await handle.close();
}
}
function stripHtmlExtension(fileName: string) {
return fileName.replace(/\.html?$/i, "");
}
function decodeHtmlEntities(value: string) {
return value
.replaceAll("&amp;", "&")
.replaceAll("&lt;", "<")
.replaceAll("&gt;", ">")
.replaceAll("&quot;", '"')
.replaceAll("&#39;", "'");
}