const fs = require("fs"); const path = require("path"); const { getRankedMeals, syncEloWithMeals } = require("./lib/elo"); const { loadMeals, repoRoot, validateMealAssets } = 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(/ `${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
", `\t\t\t\t\t`, `\t\t\t\t\t

${escapeHtml(meal.title)}

`, `\t\t\t\t\t

${escapeHtml(meal.description)}

`, "\t\t\t\t
", ].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

${meals.length} ${mealLabel} seeded at Elo ${formatRating( eloData.defaultRating )}. Enable JavaScript to vote and reorder them.

`; } 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
', `\t\t\t\t\t

#${placement}

`, `\t\t\t\t\t${escapeHtml(
      `${rankedMeal.title} thumbnail`
    )}`, '\t\t\t\t\t
', `\t\t\t\t\t\t

${escapeHtml(rankedMeal.title)}

`, `\t\t\t\t\t\t

${escapeHtml(renderRankingMeta(rankedMeal))}

`, `\t\t\t\t\t\t

${escapeHtml(rankedMeal.description)}

`, "\t\t\t\t\t
", "\t\t\t\t
", ].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()) { validateMealAssets(meals); 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) ) { validateMealAssets(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, };