4d30803b60
Add static site, shared layout, and Node dev server with standard .gitignore. Co-authored-by: Cursor <cursoragent@cursor.com>
239 lines
7.4 KiB
JavaScript
239 lines
7.4 KiB
JavaScript
/**
|
|
* One-time migration: extract nav/footer from index.html, rewrite all HTML pages.
|
|
*/
|
|
import fs from "fs";
|
|
import path from "path";
|
|
import { fileURLToPath } from "url";
|
|
import crypto from "crypto";
|
|
|
|
const ROOT = path.join(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
const COMPONENTS = path.join(ROOT, "components");
|
|
const REPORT_PATH = path.join(ROOT, "docs", "layout-migration-report.md");
|
|
|
|
const SKIP_DIRS = new Set(["temp", "node_modules", "assets", "components", "scripts", "docs"]);
|
|
const SKIP_FILES = new Set(["test.html"]);
|
|
|
|
const NAV_PARTIAL_RE =
|
|
/<!--\s*@partial\s+nav\s*-->[\s\S]*?<!--\s*@endpartial\s+nav\s*-->/i;
|
|
const NAV_LEGACY_RE =
|
|
/<!--\s*fixed-header glass-panel\s*-->\s*<nav id="nav"[\s\S]*?<\/nav>/i;
|
|
const NAV_ANY_RE = /<nav id="nav"[\s\S]*?<\/nav>/i;
|
|
|
|
const FOOTER_RE = /<footer class="pb-3 footer\s+footer-dark">[\s\S]*?<\/footer>/i;
|
|
|
|
const NAV_CONTAINER = '<div id="nav-container"></div>';
|
|
const FOOTER_CONTAINER = '<div id="footer-container"></div>';
|
|
const LAYOUT_SCRIPT = '<script src="/js/layout.js"></script>';
|
|
|
|
function collectHtmlFiles(dir, list = []) {
|
|
if (!fs.existsSync(dir)) return list;
|
|
for (const name of fs.readdirSync(dir)) {
|
|
if (SKIP_DIRS.has(name)) continue;
|
|
const full = path.join(dir, name);
|
|
const stat = fs.statSync(full);
|
|
if (stat.isDirectory()) collectHtmlFiles(full, list);
|
|
else if (name.endsWith(".html") && !SKIP_FILES.has(name)) list.push(full);
|
|
}
|
|
return list;
|
|
}
|
|
|
|
function hashContent(s) {
|
|
return crypto.createHash("sha1").update(s).digest("hex").slice(0, 10);
|
|
}
|
|
|
|
function rootifyAssetPaths(html) {
|
|
let out = html;
|
|
const rules = [
|
|
[/src="\.\/assets\//g, 'src="/assets/'],
|
|
[/src="\.\.\/assets\//g, 'src="/assets/'],
|
|
[/src="assets\//g, 'src="/assets/'],
|
|
[/href="\.\/assets\//g, 'href="/assets/'],
|
|
[/href="\.\.\/assets\//g, 'href="/assets/'],
|
|
[/href="assets\//g, 'href="/assets/'],
|
|
[/href="\.\/pages\//g, 'href="/pages/'],
|
|
[/href="\.\.\/pages\//g, 'href="/pages/'],
|
|
[/href="pages\//g, 'href="/pages/'],
|
|
[/href="\.\/index\.html"/g, 'href="/"'],
|
|
[/href="\.\.\/index\.html"/g, 'href="/"'],
|
|
[/href="\.\/"/g, 'href="/"'],
|
|
[/href="\.\.\/"/g, 'href="/"'],
|
|
];
|
|
for (const [re, rep] of rules) out = out.replace(re, rep);
|
|
return out;
|
|
}
|
|
|
|
function extractNavFromIndex(html) {
|
|
let m = html.match(NAV_PARTIAL_RE);
|
|
if (m) {
|
|
let inner = m[0]
|
|
.replace(/<!--\s*@partial\s+nav\s*-->/i, "")
|
|
.replace(/<!--\s*@endpartial\s+nav\s*-->/i, "")
|
|
.trim();
|
|
return inner;
|
|
}
|
|
m = html.match(NAV_LEGACY_RE) || html.match(NAV_ANY_RE);
|
|
return m ? m[0].replace(/<!--\s*fixed-header glass-panel\s*-->\s*/i, "").trim() : null;
|
|
}
|
|
|
|
function extractFooterFromIndex(html) {
|
|
const m = html.match(FOOTER_RE);
|
|
return m ? m[0].trim() : null;
|
|
}
|
|
|
|
function addYearSpan(footerHtml) {
|
|
return footerHtml.replace(
|
|
/©\s*\d{4}/i,
|
|
"© <span id=\"current-year\"></span>"
|
|
);
|
|
}
|
|
|
|
function stripNav(html) {
|
|
if (NAV_PARTIAL_RE.test(html)) return html.replace(NAV_PARTIAL_RE, NAV_CONTAINER);
|
|
if (NAV_LEGACY_RE.test(html)) return html.replace(NAV_LEGACY_RE, NAV_CONTAINER);
|
|
if (NAV_ANY_RE.test(html)) return html.replace(NAV_ANY_RE, NAV_CONTAINER);
|
|
return html;
|
|
}
|
|
|
|
function stripFooter(html) {
|
|
if (!FOOTER_RE.test(html)) return html;
|
|
return html.replace(FOOTER_RE, FOOTER_CONTAINER);
|
|
}
|
|
|
|
function ensureLayoutScript(html) {
|
|
if (html.includes("/js/layout.js")) return html;
|
|
if (html.includes("</body>")) {
|
|
return html.replace("</body>", ` ${LAYOUT_SCRIPT}\n</body>`);
|
|
}
|
|
return html + `\n${LAYOUT_SCRIPT}\n`;
|
|
}
|
|
|
|
function normalizeForCompare(html) {
|
|
return html.replace(/\s+/g, " ").trim();
|
|
}
|
|
|
|
function main() {
|
|
const indexPath = path.join(ROOT, "index.html");
|
|
const indexHtml = fs.readFileSync(indexPath, "utf8");
|
|
|
|
let nav = extractNavFromIndex(indexHtml);
|
|
let footer = extractFooterFromIndex(indexHtml);
|
|
if (!nav || !footer) {
|
|
console.error("Could not extract nav or footer from index.html");
|
|
process.exit(1);
|
|
}
|
|
|
|
nav = rootifyAssetPaths(nav);
|
|
footer = addYearSpan(rootifyAssetPaths(footer));
|
|
|
|
fs.mkdirSync(COMPONENTS, { recursive: true });
|
|
fs.mkdirSync(path.join(ROOT, "js"), { recursive: true });
|
|
fs.mkdirSync(path.dirname(REPORT_PATH), { recursive: true });
|
|
|
|
fs.writeFileSync(path.join(COMPONENTS, "nav.html"), nav + "\n", "utf8");
|
|
fs.writeFileSync(path.join(COMPONENTS, "footer.html"), footer + "\n", "utf8");
|
|
|
|
const files = [
|
|
indexPath,
|
|
...collectHtmlFiles(path.join(ROOT, "pages")),
|
|
...collectHtmlFiles(ROOT).filter((f) => f !== indexPath),
|
|
];
|
|
const uniqueFiles = [...new Set(files)];
|
|
|
|
const navHashes = new Map();
|
|
const footerHashes = new Map();
|
|
const report = {
|
|
canonicalNavHash: hashContent(normalizeForCompare(nav)),
|
|
canonicalFooterHash: hashContent(normalizeForCompare(footer)),
|
|
updated: [],
|
|
skipped: [],
|
|
navVariants: [],
|
|
footerVariants: [],
|
|
};
|
|
|
|
for (const file of uniqueFiles) {
|
|
let html = fs.readFileSync(file, "utf8");
|
|
const rel = path.relative(ROOT, file).replace(/\\/g, "/");
|
|
|
|
const navMatch =
|
|
html.match(NAV_PARTIAL_RE) ||
|
|
html.match(NAV_LEGACY_RE) ||
|
|
html.match(NAV_ANY_RE);
|
|
const footerMatch = html.match(FOOTER_RE);
|
|
|
|
if (navMatch) {
|
|
const h = hashContent(normalizeForCompare(rootifyAssetPaths(navMatch[0])));
|
|
navHashes.set(h, (navHashes.get(h) || 0) + 1);
|
|
if (h !== report.canonicalNavHash) {
|
|
report.navVariants.push({ file: rel, hash: h });
|
|
}
|
|
}
|
|
if (footerMatch) {
|
|
const raw = rootifyAssetPaths(footerMatch[0]);
|
|
const h = hashContent(normalizeForCompare(raw));
|
|
footerHashes.set(h, (footerHashes.get(h) || 0) + 1);
|
|
if (h !== report.canonicalFooterHash) {
|
|
report.footerVariants.push({ file: rel, hash: h });
|
|
}
|
|
}
|
|
|
|
if (!navMatch && !footerMatch) {
|
|
report.skipped.push(rel);
|
|
continue;
|
|
}
|
|
|
|
let next = html;
|
|
if (navMatch) next = stripNav(next);
|
|
if (footerMatch) next = stripFooter(next);
|
|
if (navMatch || footerMatch) next = ensureLayoutScript(next);
|
|
|
|
if (next !== html) {
|
|
fs.writeFileSync(file, next, "utf8");
|
|
report.updated.push(rel);
|
|
}
|
|
}
|
|
|
|
const navStats = [...navHashes.entries()].sort((a, b) => b[1] - a[1]);
|
|
const footerStats = [...footerHashes.entries()].sort((a, b) => b[1] - a[1]);
|
|
|
|
const md = `# Layout migration report
|
|
|
|
Generated: ${new Date().toISOString()}
|
|
|
|
## Canonical templates (from \`index.html\`)
|
|
|
|
- Nav hash: \`${report.canonicalNavHash}\`
|
|
- Footer hash: \`${report.canonicalFooterHash}\`
|
|
|
|
## Nav variant counts
|
|
|
|
${navStats.map(([h, c]) => `- \`${h}\`: ${c} page(s)`).join("\n")}
|
|
|
|
## Footer variant counts
|
|
|
|
${footerStats.map(([h, c]) => `- \`${h}\`: ${c} page(s)`).join("\n")}
|
|
|
|
## Pages with non-canonical nav (${report.navVariants.length})
|
|
|
|
${report.navVariants.length ? report.navVariants.map((v) => `- ${v.file} (\`${v.hash}\`) — TODO: review if custom nav needed`).join("\n") : "_None_"}
|
|
|
|
## Pages with non-canonical footer (${report.footerVariants.length})
|
|
|
|
${report.footerVariants.length ? report.footerVariants.map((v) => `- ${v.file} (\`${v.hash}\`) — TODO: review if custom footer needed`).join("\n") : "_None_"}
|
|
|
|
## Updated (${report.updated.length})
|
|
|
|
${report.updated.map((f) => `- ${f}`).join("\n")}
|
|
|
|
## Skipped (no nav/footer) (${report.skipped.length})
|
|
|
|
${report.skipped.map((f) => `- ${f}`).join("\n")}
|
|
`;
|
|
|
|
fs.writeFileSync(REPORT_PATH, md, "utf8");
|
|
console.log("Wrote components/nav.html, components/footer.html");
|
|
console.log("Updated", report.updated.length, "files");
|
|
console.log("Report:", REPORT_PATH);
|
|
}
|
|
|
|
main();
|