Compare commits

..

No commits in common. "d3b047cab25ae175dd63fff8799daf602b4adc7c" and "77ddefc3d1ff2e9c0787d1a0b11f7e22cb2f0f68" have entirely different histories.

3 changed files with 77 additions and 168 deletions

View file

@ -32,7 +32,6 @@
<input <input
id="toc-search" id="toc-search"
type="search" type="search"
autofocus
placeholder="Search the entries..." placeholder="Search the entries..."
/> />
<div id="toc-letters" class="toc-letters"></div> <div id="toc-letters" class="toc-letters"></div>

132
script.js
View file

@ -53,66 +53,39 @@ Array.from(firstByLetter.entries())
}); });
const searchInput = document.getElementById("toc-search"); const searchInput = document.getElementById("toc-search");
let searchTimeout;
searchInput?.addEventListener("input", (event) => { searchInput?.addEventListener("input", (event) => {
clearTimeout(searchTimeout);
searchTimeout = setTimeout(() => {
const query = event.target.value.trim().toLowerCase(); const query = event.target.value.trim().toLowerCase();
Array.from(tocList?.querySelectorAll("li") ?? []).forEach((item) => { Array.from(tocList?.querySelectorAll("li") ?? []).forEach((item) => {
const text = item.textContent.toLowerCase(); const text = item.textContent.toLowerCase();
item.style.display = text.includes(query) ? "" : "none"; item.style.display = text.includes(query) ? "" : "none";
}); });
}, 50);
}); });
// OCR/formatting errors from epub conversion const specialCaseMap = {
const typoFixes = {
"GAY MA G E": "GAY MAGE",
"MORE ON RIVERS RIVERS": "RIVERS",
"REEK OF WRONG NESS": "REEK OF WRONGNESS",
"WITCH LIGHT": "WITCHLIGHT",
};
// truncated possessives and hyphenated terms
const partialMatches = {
"A KING'S": "KINGS",
"KING'": "KINGS",
"MERCHANT'": "MERCHANTS",
"POWER-": "POWER",
"WIZARD'": "WIZARDS",
"WIZARD'S": "WIZARDS",
"WIZARD'S BREEDING PROGRAMME": "BREEDING PROGRAMMES",
"WIZARD'S STAFF": "STAFFS",
};
// British/American spelling and verb forms
const spellingVariants = {
AMBUSHED: "AMBUSHES",
BACKLASHES: "BACKLASH OF MAGIC",
CURSED: "CURSES",
"FELLOW TRAVELLER": "FELLOW TRAVELERS",
"FELLOW TRAVELLERS": "FELLOW TRAVELERS",
};
// different terms mapping to canonical entries
const synonyms = {
"A BARD": "BARDS", "A BARD": "BARDS",
"A COURTIER": "COURTIERS", "A COURTIER": "COURTIERS",
"A DEMON": "DEMONS", "A DEMON": "DEMONS",
"A GODDESS OR GOD": "GODDESSES AND GODS", "A GODDESS OR GOD": "GODDESSES AND GODS",
"A HERBWOMAN": "HERBWOMAN", "A HERBWOMAN": "HERBWOMAN",
"A KING'S": "KINGS",
"A MAGIC USER": "MAGIC USERS", "A MAGIC USER": "MAGIC USERS",
"A SEER": "SEER", "A SEER": "SEER",
"A WIZARD": "WIZARDS", "A WIZARD": "WIZARDS",
AMBUSHED: "AMBUSHES",
ASSASSIN: "ASSASSINS, GUILD OF", ASSASSIN: "ASSASSINS, GUILD OF",
ASSASSINS: "ASSASSINS, GUILD OF", ASSASSINS: "ASSASSINS, GUILD OF",
BACKLASHES: "BACKLASH OF MAGIC",
BALLAD: "BALLAD, How to Compose a", BALLAD: "BALLAD, How to Compose a",
BALLADS: "BALLAD, How to Compose a", BALLADS: "BALLAD, How to Compose a",
BLOCKED: "MOUNTAIN PASS, BLOCKED", BLOCKED: "MOUNTAIN PASS, BLOCKED",
CHILD: "CHILDREN", CHILD: "CHILDREN",
CURSED: "CURSES",
ETERNAL: "ETERNAL QUEST", ETERNAL: "ETERNAL QUEST",
"EVIL SPELLS": "SPELLS", "EVIL SPELLS": "SPELLS",
FEMALE: "SLAVES, FEMALE,", FEMALE: "SLAVES, FEMALE,",
"FELLOW TRAVELLER": "FELLOW TRAVELERS",
"FELLOW TRAVELLERS": "FELLOW TRAVELERS",
"GAY MA G E": "GAY MAGE",
"GIFT OF SIGHT": "SIGHT", "GIFT OF SIGHT": "SIGHT",
GOD: "GODDESSES AND GODS", GOD: "GODDESSES AND GODS",
GODDESS: "GODDESSES AND GODS", GODDESS: "GODDESSES AND GODS",
@ -129,90 +102,91 @@ const synonyms = {
"HOW TO INTERACT WITH WIZARDS": "WIZARDS", "HOW TO INTERACT WITH WIZARDS": "WIZARDS",
IMPORT: "IMPORT/EXPORT", IMPORT: "IMPORT/EXPORT",
"JOURNEY CAKE": "WAYBREAD OR JOURNEY CAKE", "JOURNEY CAKE": "WAYBREAD OR JOURNEY CAKE",
"KING'": "KINGS",
"LOST LAND": "LOST LANDS,", "LOST LAND": "LOST LANDS,",
"LOST LANDS": "LOST LANDS,", "LOST LANDS": "LOST LANDS,",
MAGELIGHT: "MAGELIGHT OR MAGEFIRE", MAGELIGHT: "MAGELIGHT OR MAGEFIRE",
MALE: "SLAVES, MALE,", MALE: "SLAVES, MALE,",
"MERCHANT'": "MERCHANTS",
MINION: "MINIONS OF THE DARK LORD", MINION: "MINIONS OF THE DARK LORD",
"MINION OF THE DARK LORD": "MINIONS OF THE DARK LORD", "MINION OF THE DARK LORD": "MINIONS OF THE DARK LORD",
MINIONS: "MINIONS OF THE DARK LORD", MINIONS: "MINIONS OF THE DARK LORD",
MOON: "MOON(S)",
"MORE ON RIVERS": "RIVERS", "MORE ON RIVERS": "RIVERS",
"MORE ON RIVERS RIVERS": "RIVERS",
MOON: "MOON(S)",
"MOUNTAIN PASS": "MOUNTAIN PASS, BLOCKED", "MOUNTAIN PASS": "MOUNTAIN PASS, BLOCKED",
"NORTHERN BARBARIANS DWELL": "NORTHERN BARBARIANS", "NORTHERN BARBARIANS DWELL": "NORTHERN BARBARIANS",
PANCELTIC: "PANCELTIC TOURS", PANCELTIC: "PANCELTIC TOURS",
"PANCELTIC TOUR": "PANCELTIC TOURS", "PANCELTIC TOUR": "PANCELTIC TOURS",
PENTAGRAM: "PENTAGRAM OR PENTACLE", PENTAGRAM: "PENTAGRAM OR PENTACLE",
PENTAGRAMS: "PENTAGRAM OR PENTACLE", PENTAGRAMS: "PENTAGRAM OR PENTACLE",
"POWER-": "POWER",
"PRACTICE RING": "PRACTICE RING OR COMBAT RING", "PRACTICE RING": "PRACTICE RING OR COMBAT RING",
PRIESTESSES: "HIGH PRIESTESSES", PRIESTESSES: "HIGH PRIESTESSES",
RIVERBOAT: "RIVERS",
"SECRET VALLEY": "VALLEYS", "SECRET VALLEY": "VALLEYS",
RIVERBOAT: "RIVERS",
"REEK OF WRONG NESS": "REEK OF WRONGNESS",
SLAVE: "GALLEY SLAVE", SLAVE: "GALLEY SLAVE",
SLAVES: "SLAVES, MALE,", SLAVES: "SLAVES, MALE,",
"STANDING STONES": "STONE CIRCLES", "STANDING STONES": "STONE CIRCLES",
THIEVES: "THIEVES' GUILD", THIEVES: "THIEVES GUILD",
"TOUR COMPANIONS": "COMPANIONS",
"TOWN COUNCIL": "COUNCIL", "TOWN COUNCIL": "COUNCIL",
"TOUR COMPANIONS": "COMPANIONS",
TROTS: "TROTS, THE", TROTS: "TROTS, THE",
WAYBREAD: "WAYBREAD OR JOURNEY CAKE", WAYBREAD: "WAYBREAD OR JOURNEY CAKE",
}; "WITCH LIGHT": "WITCHLIGHT",
"WIZARD'": "WIZARDS",
const specialCaseMap = { "WIZARD'S": "WIZARDS",
...typoFixes, "WIZARD'S BREEDING PROGRAMME": "BREEDING PROGRAMMES",
...partialMatches, "WIZARD'S STAFF": "STAFFS",
...spellingVariants,
...synonyms,
}; };
const resolveTerm = (term) => { const resolveTerm = (term) => {
const normalized = term.replace(/'/g, "'"); const normalized = term.replace(//g, "'");
const special = specialCaseMap[normalized]; const special = specialCaseMap[normalized];
if (special && entryMap.has(special)) { if (special && entryMap.has(special)) {
return special; return special;
} }
if (entryMap.has(normalized)) { if (entryMap.has(term)) {
return normalized; return term;
} }
const lower = normalized.toLowerCase(); const lower = term.toLowerCase();
const candidates = []; const candidates = [];
// Singularize // Singularize
if (lower.endsWith("ies")) { if (lower.endsWith("ies")) {
candidates.push(normalized.slice(0, -3) + "Y"); candidates.push(term.slice(0, -3) + "Y");
} }
if (lower.endsWith("ves")) { if (lower.endsWith("ves")) {
candidates.push(normalized.slice(0, -3) + "F"); candidates.push(term.slice(0, -3) + "F");
candidates.push(normalized.slice(0, -3) + "FE"); candidates.push(term.slice(0, -3) + "FE");
} }
if (lower.endsWith("es")) { if (lower.endsWith("es")) {
candidates.push(normalized.slice(0, -2)); candidates.push(term.slice(0, -2));
} }
if (lower.endsWith("s")) { if (lower.endsWith("s")) {
candidates.push(normalized.slice(0, -1)); candidates.push(term.slice(0, -1));
} }
// Pluralize // Pluralize
if (lower.endsWith("y") && !/[aeiou]y$/.test(lower)) { if (lower.endsWith("y") && !/[aeiou]y$/.test(lower)) {
candidates.push(normalized.slice(0, -1) + "IES"); candidates.push(term.slice(0, -1) + "IES");
} }
if (lower.endsWith("f")) { if (lower.endsWith("f")) {
candidates.push(normalized.slice(0, -1) + "VES"); candidates.push(term.slice(0, -1) + "VES");
} }
if (lower.endsWith("fe")) { if (lower.endsWith("fe")) {
candidates.push(normalized.slice(0, -2) + "VES"); candidates.push(term.slice(0, -2) + "VES");
} }
candidates.push(`${normalized}S`); candidates.push(`${term}S`);
if (/(s|x|z|ch|sh)$/i.test(lower)) { if (/(s|x|z|ch|sh)$/i.test(lower)) {
candidates.push(`${normalized}ES`); candidates.push(`${term}ES`);
} }
return candidates.find((candidate) => entryMap.has(candidate)) || null; return candidates.find((candidate) => entryMap.has(candidate)) || null;
}; };
const CAPS_TERM_REGEX = /\b[A-Z][A-Z0-9''\-]*(?:\s+[A-Z][A-Z0-9''\-]*)*\b/g;
const linkifyTextNode = (node) => { const linkifyTextNode = (node) => {
const text = node.nodeValue; const text = node.nodeValue;
if (!text || !/[A-Z]{2}/.test(text)) { if (!text || !/[A-Z]{2}/.test(text)) {
@ -220,9 +194,9 @@ const linkifyTextNode = (node) => {
} }
const matches = []; const matches = [];
CAPS_TERM_REGEX.lastIndex = 0; const regex = /\b[A-Z][A-Z0-9'\-]*(?:\s+[A-Z][A-Z0-9'\-]*)*\b/g;
let match; let match;
while ((match = CAPS_TERM_REGEX.exec(text)) !== null) { while ((match = regex.exec(text)) !== null) {
const raw = match[0]; const raw = match[0];
const cleaned = cleanTerm(raw); const cleaned = cleanTerm(raw);
const resolved = resolveTerm(cleaned); const resolved = resolveTerm(cleaned);
@ -316,38 +290,8 @@ document.body.appendChild(tocToggle);
const toc = document.querySelector(".toc"); const toc = document.querySelector(".toc");
const layout = document.querySelector(".layout"); const layout = document.querySelector(".layout");
const isMobile = () => window.matchMedia("(max-width: 700px)").matches;
tocToggle.addEventListener("click", () => { tocToggle.addEventListener("click", () => {
toc?.classList.toggle("collapsed"); toc?.classList.toggle("collapsed");
layout?.classList.toggle("toc-hidden"); layout?.classList.toggle("toc-hidden");
if (isMobile()) {
document.body.classList.toggle("toc-open");
} else {
document.body.classList.toggle("toc-collapsed"); document.body.classList.toggle("toc-collapsed");
}
});
// auto-collapse on mobile (CSS defaults to collapsed, just ensure classes match)
if (isMobile()) {
toc?.classList.add("collapsed");
layout?.classList.add("toc-hidden");
document.body.classList.remove("toc-open");
}
// close drawer when clicking a toc link on mobile
tocList?.addEventListener("click", (e) => {
if (e.target.tagName === "A" && isMobile()) {
toc?.classList.add("collapsed");
layout?.classList.add("toc-hidden");
document.body.classList.remove("toc-open");
}
});
// scroll TOC item into view when navigating via hash
window.addEventListener("hashchange", () => {
const id = location.hash.slice(1);
if (id) {
tocList?.querySelector(`a[href="#${id}"]`)?.scrollIntoView({ block: "nearest" });
}
}); });

100
style.css
View file

@ -14,13 +14,8 @@
box-sizing: border-box; box-sizing: border-box;
} }
html {
max-width: 100%;
}
body { body {
margin: 0; margin: 0;
max-width: 100%;
color: var(--ink); color: var(--ink);
background: background:
radial-gradient( radial-gradient(
@ -45,7 +40,7 @@ body {
} }
.hero { .hero {
padding: clamp(24px, 6vw, 48px) clamp(16px, 6vw, 6vw) clamp(16px, 4vw, 32px); padding: 48px 6vw 32px;
background: background:
linear-gradient(120deg, rgba(182, 82, 47, 0.12), transparent 55%), linear-gradient(120deg, rgba(182, 82, 47, 0.12), transparent 55%),
linear-gradient(220deg, rgba(122, 46, 26, 0.18), transparent 45%); linear-gradient(220deg, rgba(122, 46, 26, 0.18), transparent 45%);
@ -56,7 +51,7 @@ body {
.hero h1 { .hero h1 {
font-family: font-family:
"Iowan Old Style", "Palatino Linotype", "Book Antiqua", Garamond, serif; "Iowan Old Style", "Palatino Linotype", "Book Antiqua", Garamond, serif;
font-size: clamp(1.5rem, 6vw, 3.6rem); font-size: clamp(2.4rem, 4vw, 3.6rem);
letter-spacing: 0.02em; letter-spacing: 0.02em;
margin: 0 0 8px; margin: 0 0 8px;
} }
@ -70,15 +65,15 @@ body {
.layout { .layout {
display: grid; display: grid;
grid-template-columns: minmax(200px, 280px) minmax(0, 1fr); grid-template-columns: minmax(240px, 320px) minmax(0, 1fr);
gap: clamp(16px, 4vw, 28px); gap: 28px;
padding: clamp(16px, 4vw, 32px) clamp(12px, 6vw, 6vw) clamp(32px, 8vw, 64px); padding: 32px 6vw 64px;
} }
.toc { .toc {
background: var(--nav-bg); background: var(--nav-bg);
border-radius: 18px; border-radius: 18px;
padding: clamp(16px, 4vw, 24px); padding: 24px;
box-shadow: 0 18px 40px var(--shadow); box-shadow: 0 18px 40px var(--shadow);
position: sticky; position: sticky;
top: 24px; top: 24px;
@ -106,8 +101,8 @@ body {
} }
.toc-letters { .toc-letters {
display: grid; display: flex;
grid-template-columns: repeat(auto-fill, minmax(28px, 1fr)); flex-wrap: wrap;
gap: 6px; gap: 6px;
margin-bottom: 16px; margin-bottom: 16px;
} }
@ -115,7 +110,6 @@ body {
.toc-letters a { .toc-letters a {
font-size: 0.75rem; font-size: 0.75rem;
text-decoration: none; text-decoration: none;
text-align: center;
color: var(--accent-deep); color: var(--accent-deep);
padding: 4px 6px; padding: 4px 6px;
border-radius: 6px; border-radius: 6px;
@ -153,13 +147,10 @@ body {
.content { .content {
background: rgba(255, 255, 255, 0.6); background: rgba(255, 255, 255, 0.6);
border-radius: clamp(12px, 3vw, 24px); border-radius: 24px;
padding: clamp(16px, 4vw, 32px) clamp(16px, 5vw, 36px); padding: 32px 36px;
box-shadow: 0 20px 55px var(--shadow); box-shadow: 0 20px 55px var(--shadow);
animation: fade-up 0.8s ease 0.2s both; animation: fade-up 0.8s ease 0.2s both;
overflow-wrap: break-word;
word-wrap: break-word;
min-width: 0;
} }
.content h2, .content h2,
@ -176,7 +167,6 @@ body {
letter-spacing: 0.04em; letter-spacing: 0.04em;
animation: fade-up 0.6s ease both; animation: fade-up 0.6s ease both;
animation-delay: var(--entry-delay, 0s); animation-delay: var(--entry-delay, 0s);
scroll-margin-top: 1rem;
} }
.entry-title:target { .entry-title:target {
@ -267,65 +257,41 @@ body.toc-collapsed .toc-toggle::before {
grid-template-columns: 0 minmax(0, 1fr); grid-template-columns: 0 minmax(0, 1fr);
} }
@media (max-width: 700px) { @media (max-width: 980px) {
.page, .layout {
.hero, grid-template-columns: 1fr;
.content {
max-width: 100%;
} }
.toc { .toc {
position: fixed; position: relative;
top: 0; max-height: none;
left: 0;
bottom: 0;
width: min(85vw, 320px);
max-height: 100vh;
border-radius: 0 18px 18px 0;
z-index: 200;
margin-left: 0;
transform: translateX(-100%);
transition: transform 0.3s ease, opacity 0.3s ease;
animation: none;
} }
body.toc-open .toc { .toc.collapsed {
transform: translateX(0); margin-left: 0;
opacity: 1; margin-top: -100%;
pointer-events: auto; height: 0;
padding: 0;
overflow: hidden;
}
.layout.toc-hidden {
grid-template-columns: 1fr;
} }
.toc-toggle { .toc-toggle {
top: 50%; top: auto;
bottom: auto; bottom: 20px;
transform: translateY(-50%); left: 20px;
z-index: 201; transform: none;
transition: left 0.3s ease; border-radius: 8px;
padding: 10px 6px;
font-size: 1rem;
left: 0;
}
body.toc-open .toc-toggle {
left: min(85vw, 320px);
} }
.toc-toggle::before { .toc-toggle::before {
content: ""; content: "▲";
} }
body.toc-open .toc-toggle::before { body.toc-collapsed .toc-toggle::before {
content: "◀"; content: "▼";
}
.layout,
.layout.toc-hidden {
display: block;
grid-template-columns: none;
max-width: 100%;
}
.content {
border-radius: clamp(8px, 2vw, 16px);
} }
} }