Remove js/reading-list.js

This commit is contained in:
2026-08-26 22:36:06 +00:00
parent 7e290c5f04
commit d55eeca7d4
-113
View File
@@ -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("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;")
.replaceAll("'", "&#39;");
}
/** 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
? `<p class="reading-note">${escapeHtml(entry.note)}</p>`
: "";
// Every reading link points at an external http(s) URL (isValidUrl), so it
// carries rel="noopener noreferrer" as a hardening best practice.
return (
`<li class="reading-entry">` +
`<a class="reading-link" href="${escapeHtml(entry.url)}" rel="noopener noreferrer">${escapeHtml(entry.title)}</a>` +
note +
`</li>`
);
}
/**
* 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 '<p class="reading-empty">The reading list is empty.</p>';
}
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(
`<section class="reading-category" aria-labelledby="${id}">` +
`<h2 id="${id}">${escapeHtml(category)}</h2>` +
`<ul class="reading-list">${items.map(renderEntry).join("")}</ul>` +
`</section>`
);
}
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);
}
}