${escapeHtml(entry.title)}
${escapeHtml(entry.summary || "")}
${renderTags(entry.tags || [], "tag-list")} ${renderLinks(entry, "card-link")}(function() { const CONTENT_INDEX_PATH = "./public/content/index.json"; const escapeHtml = (value) => String(value) .replace(/&/g, "&") .replace(//g, ">") .replace(/\"/g, """) .replace(/'/g, "'"); const formatDate = (value) => { if (!value) { return ""; } const date = new Date(`${value}T00:00:00`); if (Number.isNaN(date.getTime())) { return value; } return new Intl.DateTimeFormat("en-CA", { year: "numeric", month: "short", day: "numeric" }).format(date); }; const getIndex = async () => { if (window.location.protocol === "file:") { throw new Error("This content page needs a local web server. Run python3 -m http.server and open http://localhost:8000."); } const response = await fetch(CONTENT_INDEX_PATH); if (!response.ok) { throw new Error("Unable to load content index."); } return response.json(); }; const stripFrontMatter = (markdown) => { if (!markdown.startsWith("---")) { return markdown; } const parts = markdown.split("---"); if (parts.length < 3) { return markdown; } return parts.slice(2).join("---").trim(); }; const renderTags = (tags, className) => { if (!tags || !tags.length) { return ""; } return `
${escapeHtml(entry.summary || "")}
${renderTags(entry.tags || [], "tag-list")} ${renderLinks(entry, "card-link")}${escapeHtml(emptyMessage)}
`; return; } container.innerHTML = entries.map(createCard).join(""); }; const initProjectsToggle = (entries) => { const projectsGrid = document.getElementById("projects-grid"); const toggleShell = document.getElementById("projects-toggle-shell"); const toggleButton = document.getElementById("projects-toggle"); const defaultVisibleCount = 6; if (!projectsGrid || !toggleShell || !toggleButton) { return; } const cards = Array.from(projectsGrid.querySelectorAll(".content-card")); if (entries.length <= defaultVisibleCount || cards.length <= defaultVisibleCount) { toggleShell.hidden = true; return; } const setExpandedState = (expanded) => { cards.forEach((card, index) => { card.classList.toggle("is-hidden", !expanded && index >= defaultVisibleCount); }); toggleButton.textContent = expanded ? "Show fewer projects" : "Show more projects"; toggleButton.setAttribute("aria-expanded", String(expanded)); }; toggleShell.hidden = false; setExpandedState(false); toggleButton.addEventListener("click", () => { const expanded = toggleButton.getAttribute("aria-expanded") === "true"; setExpandedState(!expanded); }); }; const compareByNewestDate = (left, right) => { const leftTime = left.date ? Date.parse(`${left.date}T00:00:00`) : Number.NEGATIVE_INFINITY; const rightTime = right.date ? Date.parse(`${right.date}T00:00:00`) : Number.NEGATIVE_INFINITY; if (leftTime !== rightTime) { return rightTime - leftTime; } const leftOrder = Number.isFinite(left.order) ? left.order : Number.MAX_SAFE_INTEGER; const rightOrder = Number.isFinite(right.order) ? right.order : Number.MAX_SAFE_INTEGER; if (leftOrder !== rightOrder) { return leftOrder - rightOrder; } return String(left.title || "").localeCompare(String(right.title || "")); }; const compareByPriorityOrder = (left, right) => { const leftOrder = Number.isFinite(left.order) ? left.order : Number.MAX_SAFE_INTEGER; const rightOrder = Number.isFinite(right.order) ? right.order : Number.MAX_SAFE_INTEGER; if (leftOrder !== rightOrder) { return leftOrder - rightOrder; } const leftTime = left.date ? Date.parse(`${left.date}T00:00:00`) : Number.NEGATIVE_INFINITY; const rightTime = right.date ? Date.parse(`${right.date}T00:00:00`) : Number.NEGATIVE_INFINITY; if (leftTime !== rightTime) { return rightTime - leftTime; } return String(left.title || "").localeCompare(String(right.title || "")); }; const initListPage = async () => { try { const index = await getIndex(); const sortedProjects = [...(index.projects || [])].sort(compareByPriorityOrder); renderSection("projects-grid", sortedProjects, "No projects added yet."); setSectionCount("projects-count", sortedProjects); initProjectsToggle(sortedProjects); const sortedUpdates = [...(index.updates || [])].sort(compareByNewestDate); renderSection("updates-grid", sortedUpdates, "No updates added yet."); setSectionCount("updates-count", sortedUpdates); } catch (error) { renderSection("projects-grid", [], error.message); renderSection("updates-grid", [], error.message); } }; const initEntryPage = async () => { const shell = document.getElementById("entry-shell"); if (!shell) { return; } try { const params = new URLSearchParams(window.location.search); const type = params.get("type"); const slug = params.get("slug"); if (!type || !slug) { shell.innerHTML = ` `; return; } const index = await getIndex(); const entries = [...(index.projects || []), ...(index.updates || [])]; const entry = entries.find((item) => item.type === type && item.slug === slug); if (!entry) { throw new Error("Entry not found."); } updateEntryMeta(entry); const response = await fetch(entry.path); if (!response.ok) { throw new Error("Unable to load markdown entry."); } const rawMarkdown = await response.text(); const markdown = stripFrontMatter(rawMarkdown); shell.innerHTML = `${escapeHtml(entry.summary || "")}
${renderTags(entry.tags || [], "entry-tag-list")}