Files
gallery/scripts/build.js

181 lines
5.0 KiB
JavaScript

const fs = require("fs");
const path = require("path");
const { getRankedMeals, syncEloWithMeals } = require("./lib/elo");
const { loadMeals, repoRoot } = require("./lib/meals");
const indexTemplatePath = path.join(repoRoot, "templates", "index.html");
const indexOutputPath = path.join(repoRoot, "index.html");
const rankingsTemplatePath = path.join(repoRoot, "templates", "rankings.html");
const rankingsOutputPath = path.join(repoRoot, "rankings.html");
const ratingFormatter = new Intl.NumberFormat("en-US", {
maximumFractionDigits: 0,
});
function detectEol(text) {
return text.includes("\r\n") ? "\r\n" : "\n";
}
function escapeHtml(value) {
return String(value)
.replace(/&/g, "&")
.replace(/>/g, ">")
.replace(/</g, "&lt;")
.replace(/"/g, "&quot;");
}
function serializeJsonForHtml(value) {
return JSON.stringify(value, null, 2)
.replace(/</g, "\\u003c")
.replace(/\u2028/g, "\\u2028")
.replace(/\u2029/g, "\\u2029");
}
function indentBlock(text, indent) {
return text
.split("\n")
.map((line) => `${indent}${line}`)
.join("\n");
}
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 formatRating(rating) {
return ratingFormatter.format(rating);
}
function renderRankingSummary(meals, eloData) {
const mealLabel = meals.length === 1 ? "meal" : "meals";
return `\t\t\t\t<p class="ranking-summary">${meals.length} ${mealLabel} seeded at Elo ${formatRating(
eloData.defaultRating
)}. Enable JavaScript to vote and reorder them.</p>`;
}
function renderRankingMeta(rankedMeal) {
const ratingText = `Elo ${formatRating(rankedMeal.rating)}`;
if (rankedMeal.matches === 0) {
return `${ratingText} | no votes yet`;
}
const matchLabel = rankedMeal.matches === 1 ? "match" : "matches";
return `${ratingText} | ${rankedMeal.wins}-${rankedMeal.losses} record across ${
rankedMeal.matches
} ${matchLabel}`;
}
function renderRankingItem(rankedMeal, placement, eol) {
return [
'\t\t\t\t<article class="ranking-card">',
`\t\t\t\t\t<p class="ranking-card__placement">#${placement}</p>`,
`\t\t\t\t\t<a class="ranking-card__thumbnail" href="images/fulls/${rankedMeal.id}.jpg"><img src="images/thumbs/${rankedMeal.id}.jpg" alt="${escapeHtml(
`${rankedMeal.title} thumbnail`
)}" /></a>`,
'\t\t\t\t\t<div class="ranking-card__body">',
`\t\t\t\t\t\t<h2>${escapeHtml(rankedMeal.title)}</h2>`,
`\t\t\t\t\t\t<p class="ranking-card__meta">${escapeHtml(renderRankingMeta(rankedMeal))}</p>`,
`\t\t\t\t\t\t<p>${escapeHtml(rankedMeal.description)}</p>`,
"\t\t\t\t\t</div>",
"\t\t\t\t</article>",
].join(eol);
}
function renderRankings(rankedMeals, eol) {
return rankedMeals
.map((rankedMeal, index) => renderRankingItem(rankedMeal, index + 1, eol))
.join(eol);
}
function renderRankingsSeedData(meals, eloData) {
return indentBlock(
serializeJsonForHtml({
meals,
elo: eloData,
}),
"\t\t\t"
);
}
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(meals = loadMeals()) {
const template = fs.readFileSync(indexTemplatePath, "utf8");
const eol = detectEol(template);
return replaceBlock(template, "gallery_items", renderGallery(meals, eol));
}
function buildRankings(
meals = loadMeals(),
eloData = syncEloWithMeals(meals)
) {
const template = fs.readFileSync(rankingsTemplatePath, "utf8");
const eol = detectEol(template);
const rankedMeals = getRankedMeals(meals, eloData);
const withSummary = replaceBlock(
template,
"ranking_summary",
renderRankingSummary(meals, eloData)
);
const withSeedData = replaceBlock(
withSummary,
"rankings_seed_data",
renderRankingsSeedData(meals, eloData)
);
return replaceBlock(withSeedData, "ranking_items", renderRankings(rankedMeals, eol));
}
function writeFile(filePath, contents) {
fs.writeFileSync(filePath, contents);
}
function main() {
const meals = loadMeals();
const eloData = syncEloWithMeals(meals);
writeFile(indexOutputPath, buildIndex(meals));
writeFile(rankingsOutputPath, buildRankings(meals, eloData));
}
function buildPages() {
main();
}
if (require.main === module) {
buildPages();
}
module.exports = {
buildPages,
buildIndex,
buildRankings,
};