Compare commits

...

4 commits

5 changed files with 60 additions and 24 deletions

1
.gitignore vendored
View file

@ -0,0 +1 @@
dist.html

22
README.txt Normal file
View file

@ -0,0 +1,22 @@
the tough guide to fantasyland
===============================
an interactive web version of diana wynne jones' satirical encyclopedia
of fantasy tropes. click any ALL CAPS term to jump to its definition.
features
--------
- auto-linking of capitalized terms to their definitions
- searchable sidebar with alphabetical navigation
- collapsible table of contents
- handles plurals, special cases, and alternate spellings
files
-----
index.html - the book content (converted from epub via calibre/tidy)
script.js - term detection, linking, toc generation
style.css - parchment aesthetic
usage
-----
just open index.html in a browser. no build step.

View file

@ -8,7 +8,7 @@
/> />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<title>The Tough Guide to Fantasyland</title> <title>The Tough Guide to Fantasyland</title>
<link rel="stylesheet" href="style.css" /> <link rel="stylesheet" href="style.css" inline />
</head> </head>
<body> <body>
<div class="page"> <div class="page">
@ -17597,9 +17597,6 @@
</main> </main>
</div> </div>
</div> </div>
<script <script type="text/javascript" src="script.js" inline></script>
type="text/javascript"
src="script.js"
></script>
</body> </body>
</html> </html>

11
justfile Normal file
View file

@ -0,0 +1,11 @@
deploy_dir := "/var/www/html/dungeon.red/fantasyland"
default: deploy
compile:
bunx inline-source-cli index.html dist.html
deploy: compile
mkdir -p {{deploy_dir}}
cp dist.html {{deploy_dir}}/index.html
@echo "deployed to {{deploy_dir}}/index.html"

View file

@ -1,3 +1,5 @@
// @ts-nocheck
const cleanTerm = (text) => text.replace(/\s+/g, " ").replace(/\.$/, "").trim(); const cleanTerm = (text) => text.replace(/\s+/g, " ").replace(/\.$/, "").trim();
const slugify = (text) => const slugify = (text) =>
@ -17,7 +19,10 @@ entrySpans.forEach((span, index) => {
const id = `entry-${slugify(term)}`; const id = `entry-${slugify(term)}`;
span.setAttribute("id", id); span.setAttribute("id", id);
span.classList.add("entry-title"); span.classList.add("entry-title");
span.style.setProperty("--entry-delay", `${Math.min(index * 0.01, 0.4)}s`); /** @type {HTMLElement} */ (span).style.setProperty(
"--entry-delay",
`${Math.min(index * 0.01, 0.4)}s`,
);
entryMap.set(term, id); entryMap.set(term, id);
}); });
@ -38,7 +43,7 @@ termEntries.forEach(([term, id]) => {
link.textContent = term; link.textContent = term;
const item = document.createElement("li"); const item = document.createElement("li");
item.appendChild(link); item.appendChild(link);
tocList.appendChild(item); tocList?.appendChild(item);
}); });
Array.from(firstByLetter.entries()) Array.from(firstByLetter.entries())
@ -47,13 +52,14 @@ Array.from(firstByLetter.entries())
const link = document.createElement("a"); const link = document.createElement("a");
link.href = `#${id}`; link.href = `#${id}`;
link.textContent = letter; link.textContent = letter;
tocLetters.appendChild(link); tocLetters?.appendChild(link);
}); });
const searchInput = document.getElementById("toc-search"); const searchInput = document.getElementById("toc-search");
searchInput.addEventListener("input", (event) => { searchInput?.addEventListener("input", (event) => {
const query = event.target.value.trim().toLowerCase(); const target = /** @type {HTMLInputElement} */ (event.target);
Array.from(tocList.querySelectorAll("li")).forEach((item) => { const query = target.value.trim().toLowerCase();
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";
}); });
@ -252,10 +258,9 @@ const linkifyTextNode = (node) => {
node.parentNode.replaceChild(fragment, node); node.parentNode.replaceChild(fragment, node);
}; };
const walker = document.createTreeWalker( const contentEl = document.getElementById("content");
document.getElementById("content"), if (contentEl) {
NodeFilter.SHOW_TEXT, const walker = document.createTreeWalker(contentEl, NodeFilter.SHOW_TEXT, {
{
acceptNode: (node) => { acceptNode: (node) => {
const parent = node.parentElement; const parent = node.parentElement;
if (!parent) { if (!parent) {
@ -269,16 +274,16 @@ const walker = document.createTreeWalker(
} }
return NodeFilter.FILTER_ACCEPT; return NodeFilter.FILTER_ACCEPT;
}, },
}, });
);
const nodesToLinkify = []; const nodesToLinkify = [];
while (walker.nextNode()) { while (walker.nextNode()) {
nodesToLinkify.push(walker.currentNode); nodesToLinkify.push(walker.currentNode);
}
nodesToLinkify.forEach((node) => linkifyTextNode(node));
} }
nodesToLinkify.forEach((node) => linkifyTextNode(node));
// TOC collapse/expand toggle // TOC collapse/expand toggle
const tocToggle = document.createElement("button"); const tocToggle = document.createElement("button");
tocToggle.className = "toc-toggle"; tocToggle.className = "toc-toggle";
@ -290,7 +295,7 @@ const toc = document.querySelector(".toc");
const layout = document.querySelector(".layout"); const layout = document.querySelector(".layout");
tocToggle.addEventListener("click", () => { tocToggle.addEventListener("click", () => {
toc.classList.toggle("collapsed"); toc?.classList.toggle("collapsed");
layout.classList.toggle("toc-hidden"); layout?.classList.toggle("toc-hidden");
document.body.classList.toggle("toc-collapsed"); document.body.classList.toggle("toc-collapsed");
}); });