import fs from "node:fs"; import path from "node:path"; const repoRoot = process.cwd(); const legacyHtmlRoot = path.join(repoRoot, "archive-source"); const contentRoot = path.join(repoRoot, "content"); const contentReadme = `# Content This directory is the future source of truth for editable site content. These Markdown files were extracted from the exported HTML posts in \`archive-source/\`. The body content intentionally keeps its original HTML markup so the migration does not lose formatting, links, headings, or embedded images. - \`content/speeches//\` - Rev. Sun Myung Moon speech posts grouped by speech year - \`content/speeches/mrs-hak-ja-han-moon/\` - Mrs. Hak Ja Han Moon speech posts - \`content/blog//\` - non-speech blog posts grouped by published year Regenerate this content from \`archive-source/blog/\` with: \`\`\`bash npm run extract:content \`\`\` `; function walk(dir) { return fs.readdirSync(dir, { withFileTypes: true }).flatMap((entry) => { const fullPath = path.join(dir, entry.name); if (entry.isDirectory()) { return walk(fullPath); } return fullPath.endsWith(".html") ? [fullPath] : []; }); } function decodeEntities(value = "") { return value .replace(/–/g, "-") .replace(/—/g, "-") .replace(/‘/g, "'") .replace(/’/g, "'") .replace(/“/g, '"') .replace(/”/g, '"') .replace(/»/g, "»") .replace(/«/g, "«") .replace(/&/g, "&") .replace(/"/g, '"') .replace(/'/g, "'") .replace(/ /g, " ") .replace(/…/g, "...") .replace(/<[^>]*>/g, "") .replace(/\s+/g, " ") .trim(); } function yamlString(value) { return JSON.stringify(value ?? ""); } function yamlArray(key, values) { if (!values.length) { return `${key}: []`; } return [`${key}:`, ...values.map((value) => ` - ${yamlString(value)}`)].join("\n"); } function matchFirst(html, pattern) { return html.match(pattern)?.[1] ?? ""; } function extractLinks(html, containerClass) { const startMatch = html.match(new RegExp(`<[^>]*class=["'][^"']*${containerClass}[^"']*["'][^>]*>`)); if (!startMatch?.index) { return []; } const start = startMatch.index + startMatch[0].length; const afterStart = html.slice(start); const endIndex = afterStart.search(containerClass === "footer-tags" ? /<\/div>/ : /<\/span>/); const body = endIndex === -1 ? afterStart : afterStart.slice(0, endIndex); return Array.from(body.matchAll(/]*href=["']([^"']+)["'][^>]*>([\s\S]*?)<\/a>/g)).map((match) => ({ href: match[1], label: decodeEntities(match[2]), })); } function extractEntryContent(html) { const content = matchFirst(html, /
\s*([\s\S]*?)\s*<\/div>/); return content .replace(/
category.href.startsWith("/speeches/categories/")); if (speechCategory) { const year = speechCategory.href.match(/(?:^|-)(1\d{3}|20\d{2})(?:-|\.html$)/)?.[1] ?? speechCategory.label.match(/\b(1\d{3}|20\d{2})\b/)?.[1]; const collection = year ?? path.basename(speechCategory.href, ".html").replace(/^rev-sun-myung-moon$/, "rev-sun-myung-moon-general"); return path.join(contentRoot, "speeches", collection, `${post.slug}.md`); } const publishYear = post.published.match(/^\d{4}/)?.[0] ?? "undated"; return path.join(contentRoot, "blog", publishYear, `${post.slug}.md`); } function extractPost(filePath) { const html = fs.readFileSync(filePath, "utf8"); const entryClass = matchFirst(html, /
/); if (!entryClass) { return null; } const title = decodeEntities(matchFirst(html, /

([\s\S]*?)<\/h1>/)); const published = matchFirst(html, /