Skip to content

Commit

Permalink
render a basic template
Browse files Browse the repository at this point in the history
  • Loading branch information
DanElliottPalmer committed Feb 10, 2024
1 parent c2a9334 commit b988bdd
Show file tree
Hide file tree
Showing 12 changed files with 781 additions and 102 deletions.
88 changes: 51 additions & 37 deletions scripts/render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,21 @@ import {
extname,
resolve,
} from "https://deno.land/std@0.209.0/path/mod.ts";
import { minify } from "npm:html-minifier@4.0.0";
import Mustache from "npm:mustache@4.2.0";
import type { CalendarMonthJson } from "../src/calendar/Calendar.ts"
import type { CalendarMonthJson } from "../src/calendar/Calendar.ts";
import { isDefined } from "../src/utils.ts";
import { PartialTemplate } from "../src/templating/PartialTemplate.ts";
import { PARTIALS_MAP } from "../src/templating/PartialsMap.ts";
import { isDefined } from "../src/utils.ts";
import { MonthTemplateData } from "../src/templating/MonthTemplateData.ts";

const __dirname = new URL(".", import.meta.url).pathname;
const TEMPLATE_DIR = resolve(__dirname, "../templates");
const OUTPUT_DIR = resolve(__dirname, "../output");

const CALENDAR_JSON_ALL_MONTHS: CalendarMonthJson = await Deno.readTextFile(
resolve(OUTPUT_DIR, "calendar-all-months.json"))
resolve(OUTPUT_DIR, "calendar-all-months.json"),
);
const CALENDAR_JSON_MONTH_FILES: ReadonlyArray<CalendarMonthJson> = [
await loadCalendarDataFile("calendar-january.json"),
await loadCalendarDataFile("calendar-february.json"),
Expand All @@ -28,83 +31,94 @@ const CALENDAR_JSON_MONTH_FILES: ReadonlyArray<CalendarMonthJson> = [
await loadCalendarDataFile("calendar-october.json"),
await loadCalendarDataFile("calendar-november.json"),
await loadCalendarDataFile("calendar-december.json"),
]
];
const FILES_TO_COPY: ReadonlyArray<string> = [
resolve(TEMPLATE_DIR, "main.css")
]
resolve(TEMPLATE_DIR, "main.css"),
];

//==============================================================================
// Execution
//==============================================================================

await run()
await run();

if(Deno.args.includes("--watch")){
console.info("Watching template directory for changes")
if (Deno.args.includes("--watch")) {
console.info("Watching template directory for changes");
const watcher = Deno.watchFs(TEMPLATE_DIR);
for await (const event of watcher) {
console.info(`Change detected in ${event.paths[0]}`)
await run()
console.info(`Change detected in ${event.paths[0]}`);
await run();
}
}

//==============================================================================
// Helpers
//==============================================================================

async function copyFiles(){
for(const src of FILES_TO_COPY){
async function copyFiles() {
for (const src of FILES_TO_COPY) {
const partialName = basename(src);
await Deno.copyFile(src, resolve(OUTPUT_DIR, partialName));
}
}

async function loadCalendarDataFile(filename: string): CalendarMonthJson {
const fileContents = await Deno.readTextFile(resolve(OUTPUT_DIR, filename))
return JSON.parse(fileContents)
const fileContents = await Deno.readTextFile(resolve(OUTPUT_DIR, filename));
return JSON.parse(fileContents);
}

async function loadAndParseTemplates() {
for await (const file of Deno.readDir(TEMPLATE_DIR)) {
if (!file.isFile) continue;
if (!file.isFile || extname(file.name) !== ".mustache") continue;
const partialName = basename(file.name, extname(file.name));
const templateFilename = resolve(TEMPLATE_DIR, file.name)
console.debug(`Loading partial: ${templateFilename}`)
const templateFilename = resolve(TEMPLATE_DIR, file.name);
console.debug(`Loading partial: ${templateFilename}`);
const fileContents = await Deno.readTextFile(templateFilename);
const partialTemplate = PartialTemplate.fromMustache(
partialName, fileContents)
partialName,
fileContents,
);
Mustache.parse(partialTemplate.markup);
PARTIALS_MAP.addTemplate(partialTemplate)
PARTIALS_MAP.addTemplate(partialTemplate);
}
console.log("PARTIALS_MAP", PARTIALS_MAP.toMarkupMap());
}

function renderIndex() {
const indexTemplate = PARTIALS_MAP.getTemplate("index")
if(!isDefined(indexTemplate)){
throw new Error(`index template is missing`)
const indexTemplate = PARTIALS_MAP.getTemplate("index");
if (!isDefined(indexTemplate)) {
throw new Error(`index template is missing`);
}
return Mustache.render(
indexTemplate.markup, CALENDAR_JSON_ALL_MONTHS, PARTIALS_MAP.toMarkupMap());
indexTemplate.markup,
CALENDAR_JSON_ALL_MONTHS,
PARTIALS_MAP.toMarkupMap(),
);
}

function renderMonth(data: CalendarMonthJson){
const monthTemplate = PARTIALS_MAP.getTemplate("month")
if(!isDefined(monthTemplate)){
throw new Error(`month template is missing`)
function renderMonth(data: CalendarMonthJson) {
const monthTemplate = PARTIALS_MAP.getTemplate("month");
if (!isDefined(monthTemplate)) {
throw new Error(`month template is missing`);
}
return Mustache.render(monthTemplate.markup, data, PARTIALS_MAP.toMarkupMap());
return Mustache.render(
monthTemplate.markup,
(new MonthTemplateData(data)).toJSON(),
PARTIALS_MAP.toMarkupMap(),
);
}

async function run(){
await copyFiles()
async function run() {
PARTIALS_MAP.clear();
await copyFiles();
await loadAndParseTemplates();
writeFile('index.html', renderIndex());
CALENDAR_JSON_MONTH_FILES.forEach(jsonFile => {
const filename = `${jsonFile.name.toLowerCase()}.html`
writeFile(filename, renderMonth(jsonFile));
})
writeFile("index.html", minify(renderIndex()));
CALENDAR_JSON_MONTH_FILES.forEach((jsonFile) => {
const filename = `${jsonFile.name.toLowerCase()}.html`;
writeFile(filename, minify(renderMonth(jsonFile)));
});
}

async function writeFile(filename: string, fileContents: string) {
await Deno.writeTextFile(resolve(OUTPUT_DIR, filename), fileContents);
}
}
229 changes: 229 additions & 0 deletions src/templating/MonthTemplateData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
import { format } from "https://deno.land/std@0.212.0/datetime/mod.ts";
import { chunk, isDefined } from "../utils.ts";
import type { CalendarMonthJson } from "../calendar/Calendar.ts";

interface CalendarDate {
date: Date;
hasGames: boolean;
isCurrentMonth: boolean;
isToday: boolean;
text: string;
url: string | null;
}

interface CalendarMonthLink {
text: string;
url: string | null;
}

interface CalendarMonth {
dates: Array<Array<CalendarDate>>;
nextMonth: CalendarMonthLink;
previousMonth: CalendarMonthLink;
text: string;
}

interface VideoGameEntry {
id: string;
platforms: Array<{ name: string, shortName: string}>
publisher?: string
releaseDate: Date;
title: string;
}

interface VideoGameSection {
date: Date;
entries: Array<VideoGameEntry>;
id: string;
title: string;
}

interface MonthTemplateDataJson {
calendar: CalendarMonth;
sections: Array<VideoGameSection>;
}

const MONTHS = [
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December",
];
const THIS_YEAR = new Date().getFullYear();

const PLURAL_RULES = new Intl.PluralRules("en-US", { type: "ordinal" });
const ORDINAL_SUFFIXES = new Map([
["one", "st"],
["two", "nd"],
["few", "rd"],
["other", "th"],
]);

export class MonthTemplateData {
#calendar: Readonly<CalendarMonth>;
#sections: Array<VideoGameSection>;

constructor(value: CalendarMonthJson) {
this.#calendar = generateCalendar(value);
this.#sections = generateSections(value);
}

toJSON(): MonthTemplateDataJson {
return {
calendar: this.#calendar,
sections: this.#sections.length > 0 ? this.#sections : [],
};
}
}

function generateCalendar(value: CalendarMonthJson): CalendarMonth {
const datesWithGames: Set<string> = new Set(
value.entries.map((entry) =>
format(new Date(entry.releaseDate), "yyyy-MM-dd")
),
);
const today = new Date();
const totalDays = new Date(2024, value.index + 1, 0, 0, 0, 0, 0).getDate();
const dates: Array<CalendarDate> = new Array(totalDays).fill(0).map(
(_, index) => {
const thisDate = new Date(2024, value.index, index + 1, 0, 0, 0, 0);
const hasGames = datesWithGames.has(format(thisDate, "yyyy-MM-dd"));
return {
date: thisDate,
hasGames,
isCurrentMonth: true,
isToday: isDateSameDay(today, thisDate),
text: thisDate.getDate().toString(),
url: hasGames ? `#day-${thisDate.getDate()}` : null,
};
},
);
// Check if we need to fill out days for the beginning of the week or end of
// the week.
// Beginning
const dayAtBeginning = offsetDay(new Date(2024, value.index, 1).getDay());
if (dayAtBeginning > 0) {
const previousDates = new Array(dayAtBeginning).fill(0).map((_, index) => {
const thisDate = new Date(2024, value.index, -index, 0, 0, 0, 0);
return {
date: thisDate,
hasGames: false,
isCurrentMonth: false,
isToday: false,
text: thisDate.getDate().toString(),
url: null,
};
});
dates.unshift(...previousDates.reverse());
}
// End
const dayAtEnd = offsetDay(new Date(2024, value.index + 1, 0).getDay());
if (dayAtEnd < 6) {
const nextDates = new Array(6 - dayAtEnd).fill(0).map((_, index) => {
const thisDate = new Date(2024, value.index + 1, index + 1, 0, 0, 0, 0);
return {
date: thisDate,
hasGames: false,
isCurrentMonth: false,
isToday: false,
text: thisDate.getDate().toString(),
url: null,
};
});
dates.push(...nextDates);
}

const previousMonthDate = new Date(2024, value.index - 1, 1, 0, 0, 0, 0);
const previousMonth = {
text: MONTHS[previousMonthDate.getMonth()],
url: previousMonthDate.getFullYear() === THIS_YEAR
? `${MONTHS[previousMonthDate.getMonth()].toLowerCase()}.html`
: null,
};

const nextMonthDate = new Date(2024, value.index + 1, 1, 0, 0, 0, 0);
const nextMonth = {
text: MONTHS[nextMonthDate.getMonth()],
url: nextMonthDate.getFullYear() === THIS_YEAR
? `${MONTHS[nextMonthDate.getMonth()].toLowerCase()}.html`
: null,
};

return {
dates: Array.from(chunk(dates, 7)),
nextMonth,
previousMonth,
text: value.name,
};
}

function generateSections(value: CalendarMonthJson): Array<VideoGameSection> {
const sortedEntries: VideoGameSection["entries"] = value.entries.map(
(entry) => {
if (isDefined(entry.uid) && isDefined(entry.name)) {
return {
id: entry.uid,
platforms: entry.platforms.slice(0),
publisher: entry.publisher,
releaseDate: new Date(entry.releaseDate),
title: entry.name,
};
}
},
).filter(<T>(entry: T): entry is NonNullable<T> => isDefined(entry)).toSorted(
(a, b) => a.releaseDate.getTime() - b.releaseDate.getTime(),
);

const sections = [];
let currentSection: VideoGameSection | undefined = undefined;
for (const entry of sortedEntries) {
if (
!isDefined(currentSection) ||
!isDateSameDay(currentSection.date, entry.releaseDate)
) {
const sectionDate = new Date(entry.releaseDate);
sectionDate.setHours(0);
sectionDate.setMinutes(0);
sectionDate.setSeconds(0);
sectionDate.setMilliseconds(0);
currentSection = {
date: sectionDate,
entries: [entry],
id: `day-${sectionDate.getDate()}`,
title: getOrdinal(sectionDate.getDate()),
};
sections.push(currentSection);
} else {
currentSection.entries.push(entry);
}
}

return sections;
}

function getOrdinal(value: number) {
const rule = PLURAL_RULES.select(value);
const suffix = ORDINAL_SUFFIXES.get(rule);
return `${value}${suffix}`;
}

function isDateSameDay(dateA: Date, dateB: Date): boolean {
return dateA.getFullYear() === dateB.getFullYear() &&
dateA.getMonth() === dateB.getMonth() &&
dateA.getDate() === dateB.getDate();
}

// Weeks start on a Monday
function offsetDay(dayIndex: number): number {
const offsetDayIndex = dayIndex - 1;
if (offsetDayIndex < 0) return 6;
return offsetDayIndex;
}
Loading

0 comments on commit b988bdd

Please sign in to comment.