|
|
import {
|
|
|
type RollTable, type RollTableAuthor, RollTableDatabase, type RollTableDetails,
|
|
|
type RollTableDetailsAndResults,
|
|
|
type RollTableResult, type RollTableResultFull, type RollTableResultSet
|
|
|
} from './rolltable';
|
|
|
import escapeHTML from 'escape-html';
|
|
|
import slug from 'slug';
|
|
|
|
|
|
export function htmlTableIdentifier(table: RollTable): string {
|
|
|
if (table.full) {
|
|
|
return slug(table.identifier);
|
|
|
} else {
|
|
|
return slug(table.header);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
export function buildFooter({ creditsUrl, includesResponses, includesGenerator }: { readonly creditsUrl: string, readonly includesResponses: boolean, readonly includesGenerator: boolean }): string {
|
|
|
return `
|
|
|
<footer>
|
|
|
${includesGenerator ? `<noscript><p>⚠️ Certain features - copy, select-all/select-none${ includesResponses ? ', reroll offline, select response' : ''} - are currently disabled because JavaScript is disabled.</p></noscript>` : '' }
|
|
|
${includesGenerator && includesResponses ? `<p class="requiresJs">💡 You can save this page to be able to generate scenarios offline!</p>` : ''}
|
|
|
<p>
|
|
|
<a href="${encodeURI(creditsUrl)}" rel="external help noreferrer">Project credits/instructions/source code</a>
|
|
|
</p>
|
|
|
</footer>`;
|
|
|
}
|
|
|
|
|
|
export function buildAuthor({ author }: { readonly author: RollTableAuthor }): string {
|
|
|
if (author.url) {
|
|
|
return `<div class="author" data-id="${escapeHTML(`${author.id}`)}"><span class="authorRelation">${escapeHTML(author.relation)}</span> <span class="authorName"><a href="${encodeURI(author.url)}" rel="external nofollow noreferrer">${escapeHTML(author.name)}</a></span></div>`;
|
|
|
} else {
|
|
|
return `<div class="author" data-id="${escapeHTML(`${author.id}`)}"><span class="authorRelation">${escapeHTML(author.relation)}</span> <span class="authorName">${escapeHTML(author.name)}</span></div>`;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
export function buildSet({ resultSet }: { readonly resultSet: RollTableResultSet }): string {
|
|
|
return `<div class="resultSet" data-id="${escapeHTML(`${resultSet.id}`)}" data-global="${resultSet.global ? 'true' : 'false'}"><span class="setRelation">in ${resultSet.name ? 'the' : 'a'} ${resultSet.global ? 'global' : 'server-local'} set</span>${resultSet.name ? ` <span class="setName">${escapeHTML(resultSet.name)}</span>` : ''}</div>`;
|
|
|
}
|
|
|
|
|
|
export function buildResultAttribution({ result }: { readonly result: RollTableResultFull<RollTable> }): string {
|
|
|
return `<div class="attribution"><div class="attributionBubble">${result.author ? buildAuthor({ author: result.author }) : ''}${buildSet({ resultSet: result.set })}</div></div>`;
|
|
|
}
|
|
|
|
|
|
export const selectedIdPrefix = 'selected-'
|
|
|
export function buildGeneratedElement({ result, selected }: { readonly result: RollTableResult, readonly selected: boolean|null }): string {
|
|
|
return (
|
|
|
`<li class="generatedElement">
|
|
|
<h2 class="generatedHead"><label class="generatedLabel tableHeader" ${buildTableData(result.table)} ${result.table.full === 'results' ? `for="${selectedIdPrefix}${htmlTableIdentifier(result.table)}"` : ''}><span class="tableEmoji">${escapeHTML(result.table.emoji)}</span> <span class="tableTitle">${escapeHTML(result.table.title)}</span></label>${selected !== null ? `<input class="generatedSelect" id="${selectedIdPrefix}${htmlTableIdentifier(result.table)}" name="${selectedIdPrefix}${htmlTableIdentifier(result.table)}" type="checkbox" ${selected ? 'checked' : ''} />` : ''}</h2>
|
|
|
<div class="generated${result.full ? ' attributed' : ''}"><button type="button" class="resultText" ${buildResultData(result)}>${escapeHTML(result.text)}</button>${result.full ? buildResultAttribution({ result }) : ''}</div>
|
|
|
</li>`)
|
|
|
}
|
|
|
|
|
|
export const submitName = "submit"
|
|
|
export const rerollId = "reroll"
|
|
|
export const rerollAllId = "rerollAll"
|
|
|
export const saveScenarioId = "saveScenario"
|
|
|
export const selectAllId = "selectAll"
|
|
|
export const selectNoneId = "selectNone"
|
|
|
export const copyMDID = "copyMD"
|
|
|
export const copyBBID = "copyBB"
|
|
|
export const copyEmojiTextID = "copyEmojiText"
|
|
|
export const copyTextID = "copyText"
|
|
|
export function buildGeneratorPage(
|
|
|
{ results, generatorTargetUrl, clientId, creditsUrl, editable, selected, includesResponses }:
|
|
|
{ readonly results: ReadonlyMap<RollTable, RollTableResult>, readonly generatorTargetUrl: string, readonly clientId: string, readonly creditsUrl: string, readonly editable: boolean, readonly selected: ReadonlySet<RollTable>, readonly includesResponses: boolean }): string {
|
|
|
return `
|
|
|
<div id="generator" class="page">
|
|
|
<form method="post" action="${encodeURI(generatorTargetUrl)}" id="generatorWindow" class="window readable">
|
|
|
<h2 id="generatorHead">Your generated scenario</h2>
|
|
|
<ul id="generatedScenario">${Array.from(results.values()).map(result => buildGeneratedElement({ result, selected: (editable && includesResponses && result.table.full === 'results') ? selected.has(result.table) : null })).join('')}</ul>
|
|
|
<div id="generatorControls">
|
|
|
<div id="copyButtons" class="buttons requiresJs jsPopupHost">
|
|
|
<button type="button" class="button" id="${copyMDID}">Markdown</button>
|
|
|
<button type="button" class="button" id="${copyBBID}">BBCode</button>
|
|
|
<button type="button" class="button" id="${copyEmojiTextID}">Text + Emoji</button>
|
|
|
<button type="button" class="button" id="${copyTextID}">Text Only</button>
|
|
|
</div>
|
|
|
${editable ? `<div id="rollButtons" class="buttons jsPopupHost">
|
|
|
<button type="submit" class="button" id="${rerollId}" name="${submitName}" value="${rerollId}">Reroll Selected</button>
|
|
|
<button type="button" class="button requiresJs" id="${selectAllId}">Select All</button>
|
|
|
<button type="button" class="button requiresJs" id="${selectNoneId}">Select None</button>
|
|
|
</div>` : ''}
|
|
|
<div id="scenarioButtons" class="buttons jsPopupHost">
|
|
|
${editable
|
|
|
? `<a href="${encodeURI(generatorTargetUrl)}" class="button" id="${rerollAllId}" draggable="false">New Scenario</a>
|
|
|
<button type="submit" class="button" id="${saveScenarioId}" name="${submitName}" value="${saveScenarioId}">Get Scenario Link</button>`
|
|
|
: `<a href="${encodeURI(generatorTargetUrl)}" class="button" draggable="false">Open in Generator</a>`}
|
|
|
</div>
|
|
|
${clientId !== '' || includesResponses ?
|
|
|
`<div id="generatorLinks" class="buttons jsPopupHost">
|
|
|
${clientId !== '' ? `<a href="https://discord.com/api/oauth2/authorize?client_id=${encodeURIComponent(clientId)}&permissions=0&scope=applications.commands" class="button" rel="external nofollow noreferrer" draggable="false">Add to Discord</a>` : ''}
|
|
|
${includesResponses ? `<a href="#responses" class="button" id="responsesLink" draggable="false">View Possible Responses</a>` : ''}
|
|
|
</div>` : ''}
|
|
|
</div>
|
|
|
</form>
|
|
|
${buildFooter({ includesResponses: includesResponses, includesGenerator: true, creditsUrl })}
|
|
|
</div>`;
|
|
|
}
|
|
|
|
|
|
export function buildResponseTypeButton({table}: {readonly table: RollTableDetails}) {
|
|
|
return `<a href="#responses-${htmlTableIdentifier(table)}" class="button" draggable="false">${escapeHTML(table.emoji)} ${escapeHTML(table.name)}</a>`
|
|
|
}
|
|
|
|
|
|
export function buildResultData(result: RollTableResult): string {
|
|
|
return result.full ? `data-mappingid="${result.mappingId}" data-textid="${result.textId}" data-updated="${result.updated.getTime()}"` : ''
|
|
|
}
|
|
|
|
|
|
export function buildTableData(table: RollTable): string {
|
|
|
return `data-ordinal="${table.ordinal}" ${table.full
|
|
|
? `data-id="${table.id}" data-identifier="${escapeHTML(table.identifier)}" data-name="${escapeHTML(table.name)}"`
|
|
|
: ''}`
|
|
|
}
|
|
|
|
|
|
export function buildResponse({result, active}: {readonly result: RollTableResult, readonly active: boolean}) {
|
|
|
return `<li class="response${active ? ' active' : ''}${result.full ? ' attributed' : ''} jsPopupHost">
|
|
|
<button type="button" class="resultText" ${buildResultData(result)}>${escapeHTML(result.text)}</button>
|
|
|
${result.full ? buildResultAttribution({result}) : ''}
|
|
|
</li>`
|
|
|
}
|
|
|
|
|
|
export function buildResponseList({table, activeResult}: {readonly table: RollTableDetailsAndResults, readonly activeResult?: RollTableResult}) {
|
|
|
return `<li class="responseType window readable" id="responses-${htmlTableIdentifier(table)}">
|
|
|
<h2 class="responseTypeHead tableHeader" ${buildTableData(table)}><span class="tableEmoji">${escapeHTML(table.emoji)}</span> <span class="tableTitle">${escapeHTML(table.title)}</span></h2>
|
|
|
<ul>
|
|
|
${Array.from(table.resultsById.values()).map(result => buildResponse({result, active: result === activeResult})).join('')}
|
|
|
</ul>
|
|
|
</li>`
|
|
|
}
|
|
|
|
|
|
export function buildResponsesPage(
|
|
|
{ tables, results, creditsUrl, includesGenerator }: {
|
|
|
readonly tables: Iterable<RollTableDetailsAndResults>,
|
|
|
readonly results?: ReadonlyMap<RollTable, RollTableResult>,
|
|
|
readonly creditsUrl: string,
|
|
|
readonly includesGenerator: boolean}): string {
|
|
|
return `
|
|
|
<div id="responses" class="page">
|
|
|
<header id="responsesHeader" class="window head">
|
|
|
<h1 id="responsesHead">Possible Responses</h1>
|
|
|
<nav class="buttons" id="responsesHeaderNav">
|
|
|
${Array.from(tables).map(table => buildResponseTypeButton({table})).join('')}
|
|
|
<a id="returnToGenerator" href="#generator" class="button" draggable="false">Return to Generator</a>
|
|
|
</nav>
|
|
|
</header>
|
|
|
<ul id="responseLists">
|
|
|
${Array.from(tables).map(table => buildResponseList({table, activeResult: results?.get(table)})).join('')}
|
|
|
</ul>
|
|
|
${buildFooter({ includesResponses: true, includesGenerator, creditsUrl })}
|
|
|
</div>
|
|
|
</body>
|
|
|
</html>`;
|
|
|
}
|
|
|
|
|
|
export function wrapPage(
|
|
|
{ title, bodyContent, script, styles, noscriptStyles }:
|
|
|
{ readonly title: string, readonly bodyContent: string, readonly script: string, readonly styles: string, readonly noscriptStyles: string }): string {
|
|
|
return `<!DOCTYPE html>
|
|
|
<html lang="en">
|
|
|
<head>
|
|
|
<meta charset="UTF-8">
|
|
|
<title>${title}</title>
|
|
|
<script>${script}</script>
|
|
|
<style>${styles}</style>
|
|
|
<noscript><style>${noscriptStyles}</style></noscript>
|
|
|
</head>
|
|
|
<body>
|
|
|
${bodyContent}
|
|
|
</body>
|
|
|
</html>`;
|
|
|
}
|
|
|
|