// @ts-nocheck const cleanTerm = (text) => text.replace(/\s+/g, " ").replace(/\.$/, "").trim(); const slugify = (text) => text .toLowerCase() .replace(/[^a-z0-9]+/g, "-") .replace(/^-+|-+$/g, ""); const entrySpans = Array.from(document.querySelectorAll("span.none2")); const entryMap = new Map(); entrySpans.forEach((span, index) => { const term = cleanTerm(span.textContent); if (!term) { return; } const id = `entry-${slugify(term)}`; span.setAttribute("id", id); span.classList.add("entry-title"); /** @type {HTMLElement} */ (span).style.setProperty( "--entry-delay", `${Math.min(index * 0.01, 0.4)}s`, ); entryMap.set(term, id); }); const tocList = document.getElementById("toc-list"); const tocLetters = document.getElementById("toc-letters"); const termEntries = Array.from(entryMap.entries()).sort(([a], [b]) => a.localeCompare(b), ); const firstByLetter = new Map(); termEntries.forEach(([term, id]) => { const letter = term[0] ? term[0].toUpperCase() : "#"; if (!firstByLetter.has(letter)) { firstByLetter.set(letter, id); } const link = document.createElement("a"); link.href = `#${id}`; link.textContent = term; const item = document.createElement("li"); item.appendChild(link); tocList?.appendChild(item); }); Array.from(firstByLetter.entries()) .sort(([a], [b]) => a.localeCompare(b)) .forEach(([letter, id]) => { const link = document.createElement("a"); link.href = `#${id}`; link.textContent = letter; tocLetters?.appendChild(link); }); const searchInput = document.getElementById("toc-search"); searchInput?.addEventListener("input", (event) => { const target = /** @type {HTMLInputElement} */ (event.target); const query = target.value.trim().toLowerCase(); Array.from(tocList?.querySelectorAll("li") ?? []).forEach((item) => { const text = item.textContent.toLowerCase(); item.style.display = text.includes(query) ? "" : "none"; }); }); const specialCaseMap = { "A BARD": "BARDS", "A COURTIER": "COURTIERS", "A DEMON": "DEMONS", "A GODDESS OR GOD": "GODDESSES AND GODS", "A HERBWOMAN": "HERBWOMAN", "A KING'S": "KINGS", "A MAGIC USER": "MAGIC USERS", "A SEER": "SEER", "A WIZARD": "WIZARDS", AMBUSHED: "AMBUSHES", ASSASSIN: "ASSASSINS, GUILD OF", ASSASSINS: "ASSASSINS, GUILD OF", BACKLASHES: "BACKLASH OF MAGIC", BALLAD: "BALLAD, How to Compose a", BALLADS: "BALLAD, How to Compose a", BLOCKED: "MOUNTAIN PASS, BLOCKED", CHILD: "CHILDREN", CURSED: "CURSES", ETERNAL: "ETERNAL QUEST", "EVIL SPELLS": "SPELLS", FEMALE: "SLAVES, FEMALE,", "FELLOW TRAVELLER": "FELLOW TRAVELERS", "FELLOW TRAVELLERS": "FELLOW TRAVELERS", "GAY MA G E": "GAY MAGE", "GIFT OF SIGHT": "SIGHT", GOD: "GODDESSES AND GODS", GODDESS: "GODDESSES AND GODS", "GODDESS OR GOD": "GODDESSES AND GODS", GODS: "GODDESSES AND GODS", "GOOD KING": "KINGS", "GOOD SPY": "SPIES", "GOOD WIZARDS": "WIZARDS", "GUILD OF": "ASSASSINS, GUILD OF", HERBWOMEN: "HERBWOMAN", HERO: "HEROES", "HIDDEN CITY": "HIDDEN KINGDOM", "HOMESPUN ROBES": "ROBES", "HOW TO INTERACT WITH WIZARDS": "WIZARDS", IMPORT: "IMPORT/EXPORT", "JOURNEY CAKE": "WAYBREAD OR JOURNEY CAKE", "KING'": "KINGS", "LOST LAND": "LOST LANDS,", "LOST LANDS": "LOST LANDS,", MAGELIGHT: "MAGELIGHT OR MAGEFIRE", MALE: "SLAVES, MALE,", "MERCHANT'": "MERCHANTS", MINION: "MINIONS OF THE DARK LORD", "MINION OF THE DARK LORD": "MINIONS OF THE DARK LORD", MINIONS: "MINIONS OF THE DARK LORD", "MORE ON RIVERS": "RIVERS", "MORE ON RIVERS RIVERS": "RIVERS", MOON: "MOON(S)", "MOUNTAIN PASS": "MOUNTAIN PASS, BLOCKED", "NORTHERN BARBARIANS DWELL": "NORTHERN BARBARIANS", PANCELTIC: "PANCELTIC TOURS", "PANCELTIC TOUR": "PANCELTIC TOURS", PENTAGRAM: "PENTAGRAM OR PENTACLE", PENTAGRAMS: "PENTAGRAM OR PENTACLE", "POWER-": "POWER", "PRACTICE RING": "PRACTICE RING OR COMBAT RING", PRIESTESSES: "HIGH PRIESTESSES", "SECRET VALLEY": "VALLEYS", RIVERBOAT: "RIVERS", "REEK OF WRONG NESS": "REEK OF WRONGNESS", SLAVE: "GALLEY SLAVE", SLAVES: "SLAVES, MALE,", "STANDING STONES": "STONE CIRCLES", THIEVES: "THIEVES’ GUILD", "TOWN COUNCIL": "COUNCIL", "TOUR COMPANIONS": "COMPANIONS", TROTS: "TROTS, THE", WAYBREAD: "WAYBREAD OR JOURNEY CAKE", "WITCH LIGHT": "WITCHLIGHT", "WIZARD'": "WIZARDS", "WIZARD'S": "WIZARDS", "WIZARD'S BREEDING PROGRAMME": "BREEDING PROGRAMMES", "WIZARD'S STAFF": "STAFFS", }; const resolveTerm = (term) => { const normalized = term.replace(/’/g, "'"); const special = specialCaseMap[normalized]; if (special && entryMap.has(special)) { return special; } if (entryMap.has(term)) { return term; } const lower = term.toLowerCase(); const candidates = []; // Singularize if (lower.endsWith("ies")) { candidates.push(term.slice(0, -3) + "Y"); } if (lower.endsWith("ves")) { candidates.push(term.slice(0, -3) + "F"); candidates.push(term.slice(0, -3) + "FE"); } if (lower.endsWith("es")) { candidates.push(term.slice(0, -2)); } if (lower.endsWith("s")) { candidates.push(term.slice(0, -1)); } // Pluralize if (lower.endsWith("y") && !/[aeiou]y$/.test(lower)) { candidates.push(term.slice(0, -1) + "IES"); } if (lower.endsWith("f")) { candidates.push(term.slice(0, -1) + "VES"); } if (lower.endsWith("fe")) { candidates.push(term.slice(0, -2) + "VES"); } candidates.push(`${term}S`); if (/(s|x|z|ch|sh)$/i.test(lower)) { candidates.push(`${term}ES`); } return candidates.find((candidate) => entryMap.has(candidate)) || null; }; const linkifyTextNode = (node) => { const text = node.nodeValue; if (!text || !/[A-Z]{2}/.test(text)) { return; } const matches = []; const regex = /\b[A-Z][A-Z0-9'’\-]*(?:\s+[A-Z][A-Z0-9'’\-]*)*\b/g; let match; while ((match = regex.exec(text)) !== null) { const raw = match[0]; const cleaned = cleanTerm(raw); const resolved = resolveTerm(cleaned); if (resolved) { matches.push({ start: match.index, end: match.index + raw.length, term: resolved, raw, }); } } if (!matches.length) { return; } // Prefer longer matches to avoid linking a short term inside a longer one. const byLength = matches .slice() .sort((a, b) => b.end - b.start - (a.end - a.start) || a.start - b.start); const selected = []; byLength.forEach((candidate) => { const overlaps = selected.some( (picked) => candidate.start < picked.end && candidate.end > picked.start, ); if (!overlaps) { selected.push(candidate); } }); selected.sort((a, b) => a.start - b.start); const fragment = document.createDocumentFragment(); let lastIndex = 0; selected.forEach(({ start, end, term }) => { if (start > lastIndex) { fragment.appendChild( document.createTextNode(text.slice(lastIndex, start)), ); } const link = document.createElement("a"); link.href = `#${entryMap.get(term)}`; link.textContent = text.slice(start, end); link.className = "entry-link"; fragment.appendChild(link); lastIndex = end; }); if (lastIndex < text.length) { fragment.appendChild(document.createTextNode(text.slice(lastIndex))); } node.parentNode.replaceChild(fragment, node); }; const contentEl = document.getElementById("content"); if (contentEl) { const walker = document.createTreeWalker(contentEl, NodeFilter.SHOW_TEXT, { acceptNode: (node) => { const parent = node.parentElement; if (!parent) { return NodeFilter.FILTER_REJECT; } if (parent.closest("a, script, style")) { return NodeFilter.FILTER_REJECT; } if (parent.closest("span.none2")) { return NodeFilter.FILTER_REJECT; } return NodeFilter.FILTER_ACCEPT; }, }); const nodesToLinkify = []; while (walker.nextNode()) { nodesToLinkify.push(walker.currentNode); } nodesToLinkify.forEach((node) => linkifyTextNode(node)); } // TOC collapse/expand toggle const tocToggle = document.createElement("button"); tocToggle.className = "toc-toggle"; tocToggle.setAttribute("aria-label", "Toggle sidebar"); tocToggle.setAttribute("title", "Toggle sidebar"); document.body.appendChild(tocToggle); const toc = document.querySelector(".toc"); const layout = document.querySelector(".layout"); tocToggle.addEventListener("click", () => { toc?.classList.toggle("collapsed"); layout?.classList.toggle("toc-hidden"); document.body.classList.toggle("toc-collapsed"); });