Files
site-portfolio/scripts/build-pages.mjs

153 lines
4.8 KiB
JavaScript

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("<article", search);
if (i === -1) return html;
const tagEnd = 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("</article>", contentStart);
if (closeIdx === -1) return html;
const afterArticle = closeIdx + "</article>".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();