125 lines
4.0 KiB
JavaScript
125 lines
4.0 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 hrefDouble = `href="${omitHrefBasename}"`;
|
|
const hrefSingle = `href='${omitHrefBasename}'`;
|
|
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 outPath = path.join(outDir, rel);
|
|
const pageDirRel = path.dirname(rel);
|
|
const depth = pageDirRel === "." ? 0 : pageDirRel.split(path.sep).length;
|
|
|
|
const vars = {
|
|
assetPrefix: depth === 0 ? "" : "../".repeat(depth),
|
|
casesPrefix: depth === 0 ? "cases/" : "",
|
|
};
|
|
|
|
const source = readUtf8(pagePath);
|
|
let rendered = renderTemplate(source, partials, vars);
|
|
const relNorm = rel.split(path.sep).join("/");
|
|
if (relNorm.startsWith("cases/")) {
|
|
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();
|