add: build script that renders index from meals
All checks were successful
Deploy on push / deploy (push) Has been skipped
All checks were successful
Deploy on push / deploy (push) Has been skipped
This commit is contained in:
90
scripts/build.js
Normal file
90
scripts/build.js
Normal file
@@ -0,0 +1,90 @@
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const repoRoot = path.resolve(__dirname, "..");
|
||||
const mealsPath = path.join(repoRoot, "data", "meals.json");
|
||||
const indexTemplatePath = path.join(repoRoot, "templates", "index.html");
|
||||
const indexOutputPath = path.join(repoRoot, "index.html");
|
||||
|
||||
function detectEol(text) {
|
||||
return text.includes("\r\n") ? "\r\n" : "\n";
|
||||
}
|
||||
|
||||
function escapeHtml(value) {
|
||||
return value
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/"/g, """);
|
||||
}
|
||||
|
||||
function validateMeals(meals) {
|
||||
if (!Array.isArray(meals)) {
|
||||
throw new Error("data/meals.json must contain an array");
|
||||
}
|
||||
|
||||
for (const [index, meal] of meals.entries()) {
|
||||
if (!meal || typeof meal !== "object") {
|
||||
throw new Error(`Meal at index ${index} must be an object`);
|
||||
}
|
||||
|
||||
for (const field of ["id", "title", "description"]) {
|
||||
if (typeof meal[field] !== "string" || meal[field].length === 0) {
|
||||
throw new Error(`Meal ${index} is missing required string field "${field}"`);
|
||||
}
|
||||
}
|
||||
|
||||
if (meal.position !== undefined && typeof meal.position !== "string") {
|
||||
throw new Error(`Meal ${index} has a non-string "position" value`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function renderGalleryItem(meal, eol) {
|
||||
const attrs = [`class="thumbnail"`, `href="images/fulls/${meal.id}.jpg"`];
|
||||
|
||||
if (meal.position) {
|
||||
attrs.push(`data-position="${escapeHtml(meal.position)}"`);
|
||||
}
|
||||
|
||||
return [
|
||||
"\t\t\t\t<article>",
|
||||
`\t\t\t\t\t<a ${attrs.join(" ")}><img src="images/thumbs/${meal.id}.jpg" alt="" /></a>`,
|
||||
`\t\t\t\t\t<h2>${escapeHtml(meal.title)}</h2>`,
|
||||
`\t\t\t\t\t<p>${escapeHtml(meal.description)}</p>`,
|
||||
"\t\t\t\t</article>",
|
||||
].join(eol);
|
||||
}
|
||||
|
||||
function renderGallery(meals, eol) {
|
||||
return meals.map((meal) => renderGalleryItem(meal, eol)).join(eol);
|
||||
}
|
||||
|
||||
function replaceBlock(template, token, replacement) {
|
||||
const pattern = new RegExp(`^[\\t ]*\\{\\{${token}\\}\\}$`, "m");
|
||||
|
||||
if (!pattern.test(template)) {
|
||||
throw new Error(`Template is missing required block token "{{${token}}}"`);
|
||||
}
|
||||
|
||||
return template.replace(pattern, () => replacement);
|
||||
}
|
||||
|
||||
function buildIndex() {
|
||||
const template = fs.readFileSync(indexTemplatePath, "utf8");
|
||||
const eol = detectEol(template);
|
||||
const meals = JSON.parse(fs.readFileSync(mealsPath, "utf8"));
|
||||
|
||||
validateMeals(meals);
|
||||
|
||||
return replaceBlock(template, "gallery_items", renderGallery(meals, eol));
|
||||
}
|
||||
|
||||
function writeFile(filePath, contents) {
|
||||
fs.writeFileSync(filePath, contents);
|
||||
}
|
||||
|
||||
function main() {
|
||||
writeFile(indexOutputPath, buildIndex());
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -1,61 +0,0 @@
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const repoRoot = path.resolve(__dirname, "..");
|
||||
const dataPath = path.join(repoRoot, "data", "meals.json");
|
||||
const indexPath = path.join(repoRoot, "index.html");
|
||||
|
||||
const startMarker = "<!-- Generated gallery items: start -->";
|
||||
const endMarker = "<!-- Generated gallery items: end -->";
|
||||
|
||||
function escapeHtml(value) {
|
||||
return value
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/\"/g, """);
|
||||
}
|
||||
|
||||
function renderArticle(entry, eol) {
|
||||
const attrs = [`class="thumbnail"`, `href="images/fulls/${entry.id}.jpg"`];
|
||||
|
||||
if (entry.position) {
|
||||
attrs.push(`data-position="${escapeHtml(entry.position)}"`);
|
||||
}
|
||||
|
||||
return [
|
||||
"\t\t\t\t<article>",
|
||||
`\t\t\t\t\t<a ${attrs.join(" ")}><img src="images/thumbs/${entry.id}.jpg" alt="" /></a>`,
|
||||
`\t\t\t\t\t<h2>${escapeHtml(entry.title)}</h2>`,
|
||||
`\t\t\t\t\t<p>${escapeHtml(entry.description)}</p>`,
|
||||
"\t\t\t\t</article>",
|
||||
].join(eol);
|
||||
}
|
||||
|
||||
function renderGallery(entries, eol) {
|
||||
return entries.map((entry) => renderArticle(entry, eol)).join(eol);
|
||||
}
|
||||
|
||||
function main() {
|
||||
const entries = JSON.parse(fs.readFileSync(dataPath, "utf8"));
|
||||
const indexHtml = fs.readFileSync(indexPath, "utf8");
|
||||
const eol = indexHtml.includes("\r\n") ? "\r\n" : "\n";
|
||||
const galleryMarkup = renderGallery(entries, eol);
|
||||
const replacement = [
|
||||
`\t\t\t\t${startMarker}`,
|
||||
galleryMarkup,
|
||||
`\t\t\t\t${endMarker}`,
|
||||
].join(eol);
|
||||
|
||||
const markerPattern = new RegExp(
|
||||
`^[\\t ]*${startMarker}[\\s\\S]*?^[\\t ]*${endMarker}`,
|
||||
"m"
|
||||
);
|
||||
|
||||
if (!markerPattern.test(indexHtml)) {
|
||||
throw new Error("Could not find gallery markers in index.html");
|
||||
}
|
||||
|
||||
fs.writeFileSync(indexPath, indexHtml.replace(markerPattern, replacement));
|
||||
}
|
||||
|
||||
main();
|
||||
Reference in New Issue
Block a user