188 lines
5.8 KiB
TypeScript
188 lines
5.8 KiB
TypeScript
import { Playbill, type TaggedComponent } from "@proscenium/playbill";
|
|
import sortBy from "lodash-es/sortBy";
|
|
import { createMarkdownRenderer, type BoundPlaybill } from "#lib";
|
|
import { GlossaryTable } from "./component/glossary";
|
|
import { PageHeader } from "./component/header";
|
|
import { ItemCard } from "./component/item";
|
|
import { MethodSection } from "./component/method";
|
|
import { ResourceSection } from "./component/resource";
|
|
import { RuleSection } from "./component/rule";
|
|
import { SpeciesSection } from "./component/species";
|
|
import { renderToString } from "react-dom/server";
|
|
import defaultCSS from "./default.css" with { type: "text" };
|
|
|
|
export async function renderPlaybillToHTML(
|
|
boundPlaybill: BoundPlaybill,
|
|
): Promise<string> {
|
|
// Make a copy of the playbill to avoid modifying the original
|
|
// and compile all description markdown
|
|
const playbill = Playbill.fromSerialized(boundPlaybill.playbill.serialize());
|
|
|
|
// Prepare a rendering function for markdown that is aware of the playbill binding
|
|
const renderMarkdown = createMarkdownRenderer(boundPlaybill);
|
|
|
|
// Define a processor function to iterate over all components and process their markdown
|
|
const processMarkdownInComponent = async (entry: TaggedComponent) => {
|
|
entry.component.description = await renderMarkdown(
|
|
entry.component.description,
|
|
);
|
|
|
|
if (entry.type === "resource" && entry.component.type === "table") {
|
|
const newData: string[][] = [];
|
|
for (const row of entry.component.data) {
|
|
const newRow: string[] = [];
|
|
for (const cell of row) {
|
|
newRow.push(await renderMarkdown(cell));
|
|
}
|
|
newData.push(newRow);
|
|
}
|
|
entry.component.data = newData;
|
|
}
|
|
};
|
|
|
|
// Process all components
|
|
await playbill.processComponents(processMarkdownInComponent);
|
|
|
|
// Soprt rules by their order prop
|
|
const rulesByOrder = sortBy(Object.values(playbill.rules), "order");
|
|
|
|
// Prepare stylesheet
|
|
const cssParts: string[] = [];
|
|
|
|
if (!boundPlaybill.omitDefaultStyles) {
|
|
cssParts.push(`<style>${defaultCSS}</style>`);
|
|
}
|
|
|
|
if (boundPlaybill.styles.length > 0) {
|
|
cssParts.push(`<style>${boundPlaybill.styles}</style>`);
|
|
}
|
|
|
|
const css = cssParts.join("\n");
|
|
|
|
const body = renderToString(
|
|
<>
|
|
<article id="species" className="view">
|
|
{Object.values(playbill.species).map((species) => (
|
|
<SpeciesSection
|
|
key={species.id}
|
|
species={species}
|
|
playbill={playbill}
|
|
/>
|
|
))}
|
|
</article>
|
|
|
|
<article id="methods" className="view" style={{ display: "none" }}>
|
|
{Object.values(playbill.methods).map((method) => (
|
|
<MethodSection key={method.id} method={method} playbill={playbill} />
|
|
))}
|
|
</article>
|
|
|
|
<article id="items" className="view" style={{ display: "none" }}>
|
|
{Object.values(playbill.items).map((item) => (
|
|
<ItemCard key={item.id} item={item} />
|
|
))}
|
|
</article>
|
|
|
|
<article id="rules" className="view" style={{ display: "none" }}>
|
|
{rulesByOrder.map((rule) => (
|
|
<RuleSection key={rule.id} rule={rule} />
|
|
))}
|
|
</article>
|
|
|
|
<article id="glossary" className="view" style={{ display: "none" }}>
|
|
{<GlossaryTable glossary={playbill.glossary} />}
|
|
</article>
|
|
|
|
<article id="resources" className="view" style={{ display: "none" }}>
|
|
{Object.values(playbill.resources).map((resource) => (
|
|
<ResourceSection key={resource.id} resource={resource} />
|
|
))}
|
|
</article>
|
|
</>,
|
|
);
|
|
|
|
// Main document
|
|
const doc = `
|
|
<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>Playbill: ${playbill.name}</title>
|
|
${css}
|
|
|
|
<script>
|
|
function removeHash() {
|
|
history.pushState("", document.title, window.location.pathname);
|
|
}
|
|
|
|
function setHash(hash) {
|
|
if (hash.length > 0) {
|
|
history.pushState(null, null, "#" + hash);
|
|
} else {
|
|
removeHash();
|
|
}
|
|
}
|
|
|
|
function hideAllViews() {
|
|
document.querySelectorAll(".view").forEach((view) => {
|
|
view.style.display = "none";
|
|
});
|
|
}
|
|
|
|
function showAllViews() {
|
|
document.querySelectorAll(".view").forEach((view) => {
|
|
view.style.display = "block";
|
|
});
|
|
setHash("");
|
|
}
|
|
|
|
function showView(viewId) {
|
|
document.getElementById(viewId).style.display = "block";
|
|
setHash(viewId);
|
|
}
|
|
|
|
|
|
function hideAllAndShow(viewId) {
|
|
hideAllViews();
|
|
showView(viewId);
|
|
}
|
|
|
|
function navigateFromHash() {
|
|
const hash = window.location.hash.slice(1);
|
|
if (hash.length > 0) {
|
|
const view = document.getElementById(hash);
|
|
|
|
if (view !== null) {
|
|
hideAllAndShow(hash);
|
|
} else {
|
|
showAllViews();
|
|
}
|
|
} else {
|
|
showAllViews();
|
|
}
|
|
}
|
|
|
|
document.addEventListener("DOMContentLoaded", navigateFromHash());
|
|
window.addEventListener("hashchange", navigateFromHash())
|
|
</script>
|
|
</head>
|
|
<body>
|
|
${renderToString(<PageHeader playbill={playbill} />)}
|
|
<blockquote class="overview"><p>${playbill.description}</p></blockquote>
|
|
<nav>
|
|
<button onclick="hideAllAndShow('species')" id="species-nav">Species</button>
|
|
<button onclick="hideAllAndShow('methods')" id="methods-nav">Methods</button>
|
|
<button onclick="hideAllAndShow('items')" id="items-nav">Items</button>
|
|
<button onclick="hideAllAndShow('rules')" id="rules-nav">Rulebook</button>
|
|
<button onclick="hideAllAndShow('glossary')" id="glossary-nav">Glossary</button>
|
|
<button onclick="hideAllAndShow('resources')" id="resources-nav">Resources</button>
|
|
<button onclick="showAllViews()" id="all-nav">Show All</button>
|
|
</nav>
|
|
${body}
|
|
</body>
|
|
</html>
|
|
`;
|
|
|
|
return doc;
|
|
}
|