import fs from "node:fs"; import path from "node:path"; const rootDir = process.cwd(); const srcPagesDir = path.join(rootDir, "src", "pages"); const srcPartialsDir = path.join(rootDir, "src", "partials"); const outDir = path.join(rootDir, "public"); const includeRe = /\{\{\>\s*([a-z0-9-]+)\s*\}\}/gi; const varRe = /\{\{\s*([a-zA-Z0-9_]+)\s*\}\}/g; function readUtf8(filePath) { return fs.readFileSync(filePath, "utf8"); } function walkHtmlFiles(dir, acc = []) { const entries = fs.readdirSync(dir, { withFileTypes: true }); for (const entry of entries) { const abs = path.join(dir, entry.name); if (entry.isDirectory()) { walkHtmlFiles(abs, acc); continue; } if (entry.isFile() && entry.name.endsWith(".html")) { acc.push(abs); } } return acc; } function loadPartials() { const partialMap = new Map(); for (const filePath of walkHtmlFiles(srcPartialsDir)) { const name = path.basename(filePath, ".html"); partialMap.set(name, readUtf8(filePath)); } return partialMap; } function renderTemplate(input, partials, vars, stack = []) { const withIncludes = input.replace(includeRe, (_, name) => { const key = String(name).trim(); if (!partials.has(key)) { throw new Error(`Partial "${key}" not found`); } if (stack.includes(key)) { throw new Error(`Circular partial include: ${[...stack, key].join(" -> ")}`); } const partialContent = partials.get(key); return renderTemplate(partialContent, partials, vars, [...stack, key]); }); return withIncludes.replace(varRe, (_, name) => { const key = String(name).trim(); if (Object.prototype.hasOwnProperty.call(vars, key)) { return String(vars[key]); } return `{{${key}}}`; }); } function ensureDir(dirPath) { fs.mkdirSync(dirPath, { recursive: true }); } /** На странице кейса убираем из HTML карусели карточку со ссылкой на текущий файл (href уже с подставленным casesPrefix). */ function stripCurrentCasePortfolioCarouselItem(html, omitHrefBasename) { if (!omitHrefBasename) return html; const slug = omitHrefBasename.replace(/\.html$/, ""); const hrefDouble = `href="${slug}/"`; const hrefSingle = `href='${slug}/'`; const itemClass = "projects-carousel__item"; let search = 0; while (true) { const i = html.indexOf("", i); if (tagEnd === -1) return html; const openTag = html.slice(i, tagEnd + 1); if (!openTag.includes(itemClass)) { search = i + 9; continue; } const contentStart = tagEnd + 1; const closeIdx = html.indexOf("", contentStart); if (closeIdx === -1) return html; const afterArticle = closeIdx + "".length; const block = html.slice(i, afterArticle); if (block.includes(hrefDouble) || block.includes(hrefSingle)) { return html.slice(0, i) + html.slice(afterArticle); } search = contentStart; } } function build() { const partials = loadPartials(); const pageFiles = walkHtmlFiles(srcPagesDir); for (const pagePath of pageFiles) { const rel = path.relative(srcPagesDir, pagePath); const relNorm = rel.split(path.sep).join("/"); const pageDirRel = path.dirname(rel); const depth = pageDirRel === "." ? 0 : pageDirRel.split(path.sep).length; const isCase = relNorm.startsWith("cases/"); const isResume = relNorm === "resume.html"; const isCookiePolicy = relNorm === "cookie-policy.html"; let outPath; let vars; if (isCase) { const slug = path.basename(pagePath, ".html"); outPath = path.join(outDir, "cases", slug, "index.html"); vars = { assetPrefix: "../../", casesPrefix: "../", }; } else if (isResume) { outPath = path.join(outDir, "resume", "index.html"); vars = { assetPrefix: "../", casesPrefix: "", }; } else if (isCookiePolicy) { outPath = path.join(outDir, "cookie-policy", "index.html"); vars = { assetPrefix: "../", casesPrefix: "", }; } else { outPath = path.join(outDir, rel); vars = { assetPrefix: depth === 0 ? "" : "../".repeat(depth), casesPrefix: depth === 0 ? "cases/" : "", }; } const source = readUtf8(pagePath); let rendered = renderTemplate(source, partials, vars); if (isCase) { const omitFile = path.basename(pagePath); rendered = stripCurrentCasePortfolioCarouselItem(rendered, omitFile); } ensureDir(path.dirname(outPath)); fs.writeFileSync(outPath, rendered, "utf8"); } console.log(`Built ${pageFiles.length} pages into public/`); } build();