From d55eeca7d42a7c3acd16329a455538490f3a7e38 Mon Sep 17 00:00:00 2001 From: kpcto Date: Wed, 26 Aug 2026 22:36:06 +0000 Subject: [PATCH] Remove js/reading-list.js --- js/reading-list.js | 113 --------------------------------------------- 1 file changed, 113 deletions(-) delete mode 100644 js/reading-list.js diff --git a/js/reading-list.js b/js/reading-list.js deleted file mode 100644 index 7c1657f..0000000 --- a/js/reading-list.js +++ /dev/null @@ -1,113 +0,0 @@ -/** - * Reading list renderer — turns the curated reading list data file into - * category-grouped HTML. Pure string builder (no DOM), so it can be unit - * tested in Node; the browser wiring at the bottom is guarded accordingly. - */ -import { READING_LIST } from "../data/reading-list.js"; - -/** HTML-escape a value so it is safe to embed in markup. */ -export function escapeHtml(value) { - return String(value) - .replaceAll("&", "&") - .replaceAll("<", "<") - .replaceAll(">", ">") - .replaceAll('"', """) - .replaceAll("'", "'"); -} - -/** True when the value is an absolute http(s) URL. */ -export function isValidUrl(value) { - try { - const url = new URL(String(value)); - return url.protocol === "http:" || url.protocol === "https:"; - } catch { - return false; - } -} - -/** True when the entry has everything needed to render. */ -export function isValidEntry(entry) { - return Boolean( - entry && - typeof entry.title === "string" && - entry.title.length > 0 && - isValidUrl(entry.url) - ); -} - -/** - * Group valid entries by category, preserving first-seen category order and - * entry order within each category. Entries without a category fall under - * "Uncategorized". Invalid entries are skipped. - */ -export function groupByCategory(entries) { - const groups = new Map(); - for (const entry of entries) { - if (!isValidEntry(entry)) continue; - const category = entry.category || "Uncategorized"; - if (!groups.has(category)) groups.set(category, []); - groups.get(category).push(entry); - } - return groups; -} - -/** Slugify a category name into a stable, unique HTML id. */ -function slugify(value) { - return value - .toLowerCase() - .replace(/[^a-z0-9]+/g, "-") - .replace(/^-+|-+$/g, ""); -} - -/** Render one entry: a title link plus an optional one-line note. */ -function renderEntry(entry) { - const note = entry.note - ? `

${escapeHtml(entry.note)}

` - : ""; - // Every reading link points at an external http(s) URL (isValidUrl), so it - // carries rel="noopener noreferrer" as a hardening best practice. - return ( - `
  • ` + - `${escapeHtml(entry.title)}` + - note + - `
  • ` - ); -} - -/** - * Render the full reading list as an HTML string, grouped by category. - * - * @param {Array<{title: string, url: string, note?: string, category?: string}>} entries - * @returns {string} HTML string ready to be injected into `#reading-list` - */ -export function renderReadingList(entries) { - const groups = groupByCategory(entries); - if (groups.size === 0) { - return '

    The reading list is empty.

    '; - } - - const usedIds = new Set(); - const sections = []; - for (const [category, items] of groups) { - let id = slugify(category) || "category"; - while (usedIds.has(id)) id = `${id}-${usedIds.size + 1}`; - usedIds.add(id); - - sections.push( - `
    ` + - `

    ${escapeHtml(category)}

    ` + - `` + - `
    ` - ); - } - return sections.join(""); -} - -// Browser-only wiring — render the data file into the page. Guarded so this -// module stays importable (and testable) in Node. -if (typeof window !== "undefined" && typeof document !== "undefined") { - const container = document.getElementById("reading-list"); - if (container) { - container.innerHTML = renderReadingList(READING_LIST); - } -}