Files
deckyvault/apps/web/lib/updates.ts
T
adrianbonpin cd72b7a948 refactor: convert to bun workspaces monorepo
- Move web app into apps/web/
- Create packages/shared/ with shared types
- Create plugins/decky-vault/ scaffold
- Root package.json manages workspaces only
2026-06-28 05:20:28 +08:00

123 lines
3.1 KiB
TypeScript

import fs from "fs"
import path from "path"
import matter from "gray-matter"
import { remark } from "remark"
import remarkRehype from "remark-rehype"
import rehypeSlug from "rehype-slug"
import rehypeAutolinkHeadings from "rehype-autolink-headings"
import rehypeStringify from "rehype-stringify"
export interface UpdateMeta {
slug: string
title: string
date: string
version: string
summary: string
}
export interface UpdateHeading {
id: string
text: string
level: number
}
export interface UpdateContent {
meta: UpdateMeta
html: string
headings: UpdateHeading[]
}
const UPDATES_DIR = path.join(process.cwd(), "content", "updates")
function getSlugs(): string[] {
if (!fs.existsSync(UPDATES_DIR)) return []
return fs
.readdirSync(UPDATES_DIR)
.filter((file) => file.endsWith(".md"))
.map((file) => file.replace(/\.md$/, ""))
}
function compareVersion(a: string, b: string): number {
const pa = a.split(".").map(Number)
const pb = b.split(".").map(Number)
const len = Math.max(pa.length, pb.length)
for (let i = 0; i < len; i++) {
const na = pa[i] ?? 0
const nb = pb[i] ?? 0
if (na !== nb) return nb - na // descending
}
return 0
}
export function getAllUpdates(): UpdateMeta[] {
const slugs = getSlugs()
const updates = slugs.map((slug) => {
const { meta } = getUpdateMeta(slug)
return meta
})
// Sort by date descending, then by version descending (newest first)
return updates.sort((a, b) => {
const dateDiff = new Date(b.date).getTime() - new Date(a.date).getTime()
if (dateDiff !== 0) return dateDiff
return compareVersion(a.version, b.version)
})
}
function getUpdateMeta(slug: string): { meta: UpdateMeta } {
const filePath = path.join(UPDATES_DIR, `${slug}.md`)
const fileContents = fs.readFileSync(filePath, "utf8")
const { data } = matter(fileContents)
return {
meta: {
slug,
title: data.title ?? "",
date: data.date ?? "",
version: data.version ?? "",
summary: data.summary ?? "",
},
}
}
export async function getUpdateBySlug(slug: string): Promise<UpdateContent> {
const filePath = path.join(UPDATES_DIR, `${slug}.md`)
const fileContents = fs.readFileSync(filePath, "utf8")
const { data, content } = matter(fileContents)
const processedContent = await remark()
.use(remarkRehype)
.use(rehypeSlug)
.use(rehypeAutolinkHeadings)
.use(rehypeStringify)
.process(content)
const html = processedContent.toString()
// Extract headings from the rendered HTML
const headingRegex = /<h([1-6])[^>]*id=["']([^"']+)["'][^>]*>(.*?)<\/h[1-6]>/g
const headings: UpdateHeading[] = []
let match: RegExpExecArray | null
while ((match = headingRegex.exec(html)) !== null) {
headings.push({
level: parseInt(match[1], 10),
text: match[3].replace(/<[^>]*>/g, ""), // Strip any inner HTML tags
id: match[2],
})
}
return {
meta: {
slug,
title: data.title ?? "",
date: data.date ?? "",
version: data.version ?? "",
summary: data.summary ?? "",
},
html,
headings,
}
}
export function getAllUpdateSlugs(): string[] {
return getSlugs()
}