Initial commit: AIFO official portal web.
Add static site, shared layout, and Node dev server with standard .gitignore. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,238 @@
|
||||
/**
|
||||
* 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();
|
||||
Reference in New Issue
Block a user