import { readdir, readFile, writeFile } from 'node:fs/promises'; import { basename, extname, join, relative } from 'node:path'; const POSTS_DIR = join(process.cwd(), 'source', '_posts'); const CRC32_TABLE = new Uint32Array(256); for (let i = 0; i < 256; i += 1) { let value = i; for (let bit = 0; bit < 8; bit += 1) { value = value & 1 ? 0xedb88320 ^ (value >>> 1) : value >>> 1; } CRC32_TABLE[i] = value >>> 0; } function crc32(input) { const bytes = new TextEncoder().encode(input); let crc = 0xffffffff; for (const byte of bytes) { crc = CRC32_TABLE[(crc ^ byte) & 0xff] ^ (crc >>> 8); } return ((crc ^ 0xffffffff) >>> 0).toString(16); } async function listMarkdownFiles(dir) { const entries = await readdir(dir, { withFileTypes: true }); const files = await Promise.all( entries.map(async (entry) => { const path = join(dir, entry.name); if (entry.isDirectory()) return listMarkdownFiles(path); if (entry.isFile() && extname(entry.name).toLowerCase() === '.md') return [path]; return []; }), ); return files.flat(); } function getFrontmatterBounds(content) { if (!content.startsWith('---')) return null; const newline = content.includes('\r\n') ? '\r\n' : '\n'; const lines = content.split(newline); const closeIndex = lines.findIndex((line, index) => index > 0 && line.trim() === '---'); if (closeIndex === -1) return null; return { closeIndex, lines, newline }; } function getScalar(frontmatterLines, key) { const pattern = new RegExp(`^${key}\\s*:\\s*(.*)$`); const match = frontmatterLines.map((line) => line.match(pattern)).find(Boolean); if (!match) return ''; return match[1] .trim() .replace(/^['"]|['"]$/g, '') .trim(); } function makeSeed(file, frontmatterLines) { const title = getScalar(frontmatterLines, 'title'); if (title) return title; const filename = getScalar(frontmatterLines, 'filename'); if (filename) return filename; return basename(file, '.md'); } function makeUniqueAbbrlink(seed, usedAbbrlinks) { let attempt = 0; let abbrlink = crc32(seed); while (usedAbbrlinks.has(abbrlink)) { attempt += 1; abbrlink = crc32(`${seed}:${attempt}`); } usedAbbrlinks.add(abbrlink); return abbrlink; } async function main() { const files = await listMarkdownFiles(POSTS_DIR); const usedAbbrlinks = new Set(); const pending = []; for (const file of files) { const content = await readFile(file, 'utf8'); const bounds = getFrontmatterBounds(content); if (!bounds) continue; const frontmatterLines = bounds.lines.slice(1, bounds.closeIndex); const existing = getScalar(frontmatterLines, 'abbrlink'); if (existing) { usedAbbrlinks.add(existing); continue; } pending.push({ file, content, bounds, frontmatterLines }); } for (const item of pending) { const seed = makeSeed(item.file, item.frontmatterLines); const abbrlink = makeUniqueAbbrlink(seed, usedAbbrlinks); const insertAt = item.bounds.closeIndex; item.bounds.lines.splice(insertAt, 0, `abbrlink: ${abbrlink}`); await writeFile(item.file, item.bounds.lines.join(item.bounds.newline), 'utf8'); console.log(`Added abbrlink ${abbrlink} to ${relative(process.cwd(), item.file)}`); } if (pending.length === 0) { console.log('All posts already have abbrlink.'); } } main().catch((error) => { console.error(error); process.exitCode = 1; });