improvements
All checks were successful
ci/woodpecker/cron/ci Pipeline was successful

This commit is contained in:
2026-01-18 23:02:18 -05:00
parent c54b1a6082
commit 9332ac6f94
3 changed files with 322 additions and 90 deletions

View File

@@ -2,6 +2,18 @@ function pickRandom(arr) {
return arr[Math.floor(Math.random() * arr.length)];
}
function escapeHtml(text) {
if (!text) return '';
const map = {
'&': '&',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#039;'
};
return String(text).replace(/[&<>"']/g, m => map[m]);
}
export function dungeonTemplate(data) {
const bodyFonts = [
"'Lora', serif",
@@ -51,15 +63,15 @@ export function dungeonTemplate(data) {
padding: 0;
font-family: ${bodyFont};
color: #1a1a1a;
font-size: 0.65em;
line-height: 1.2em;
font-size: 0.75em;
line-height: 1.4em;
}
.content-page {
height: 100vh;
box-sizing: border-box;
padding: 1.2cm;
page-break-after: always;
overflow: hidden;
overflow: visible;
break-inside: avoid;
}
h1 {
@@ -79,7 +91,7 @@ export function dungeonTemplate(data) {
font-family: ${quoteFont};
margin: 0.3em 0 0.6em;
font-size: 0.85em;
line-height: 1.2em;
line-height: 1.4em;
}
.columns {
display: grid;
@@ -90,18 +102,19 @@ export function dungeonTemplate(data) {
.col {
display: flex;
flex-direction: column;
gap: 0.15em;
gap: 0.3em;
overflow-wrap: break-word;
word-break: break-word;
word-break: normal;
hyphens: auto;
}
.section-block {
break-inside: avoid;
page-break-inside: avoid;
margin-bottom: 0.25em;
margin-bottom: 0.4em;
}
h2 {
font-family: ${headingFont};
font-size: 0.95em;
font-size: 1.05em;
margin: 0.2em 0 0.2em;
color: #1a1a1a;
border-bottom: 1px solid #1a1a1a;
@@ -117,28 +130,28 @@ export function dungeonTemplate(data) {
}
.room h3 {
margin: 0.15em 0 0.08em;
font-size: 0.85em;
font-size: 0.95em;
font-weight: bold;
color: #1a1a1a;
}
.room p {
margin: 0 0 0.35em;
font-size: 0.8em;
font-size: 0.9em;
font-weight: normal;
line-height: 1.25em;
line-height: 1.35em;
}
.encounter, .npc, .treasure, .plot-resolution {
margin: 0 0 0.3em;
margin: 0 0 0.35em;
break-inside: avoid;
page-break-inside: avoid;
font-size: 0.8em;
line-height: 1.25em;
font-size: 0.9em;
line-height: 1.35em;
}
.random-events {
margin: 0.2em 0;
break-inside: avoid;
page-break-inside: avoid;
font-size: 0.8em;
font-size: 0.9em;
}
.random-events table {
margin-top: 0.15em;
@@ -150,7 +163,7 @@ export function dungeonTemplate(data) {
width: 100%;
border-collapse: collapse;
margin: 0.2em 0;
font-size: 0.8em;
font-size: 0.85em;
break-inside: avoid;
page-break-inside: avoid;
border: 1px solid #1a1a1a;
@@ -165,12 +178,13 @@ export function dungeonTemplate(data) {
font-weight: bold;
}
table td {
padding: 0.2em 0.3em;
padding: 0.25em 0.4em;
vertical-align: top;
line-height: 1.3em;
border-bottom: 1px solid #1a1a1a;
word-wrap: break-word;
overflow-wrap: break-word;
word-break: normal;
}
table tr:last-child td {
border-bottom: 1px solid #1a1a1a;
@@ -183,14 +197,15 @@ export function dungeonTemplate(data) {
}
.encounters-table td:nth-child(2) {
font-weight: bold;
width: 25%;
min-width: 20%;
max-width: 30%;
padding-right: 0.5em;
border-right: 1px solid #1a1a1a;
}
.encounters-table td:nth-child(3) {
width: auto;
font-size: 0.75em;
line-height: 1.2em;
font-size: 0.8em;
line-height: 1.3em;
}
.map-page {
display: flex;
@@ -213,15 +228,15 @@ export function dungeonTemplate(data) {
}
li {
margin: 0.08em 0;
font-size: 0.8em;
line-height: 1.25em;
font-size: 0.9em;
line-height: 1.35em;
}
</style>
</head>
<body>
<div class="content-page">
<h1>${data.title}</h1>
${data.flavor ? `<p class="flavor">${data.flavor}</p>` : ''}
<h1>${escapeHtml(data.title)}</h1>
${data.flavor ? `<p class="flavor">${escapeHtml(data.flavor)}</p>` : ''}
<div class="columns">
<div class="col">
@@ -229,7 +244,7 @@ export function dungeonTemplate(data) {
<div class="section-block">
<h2>Hooks & Rumors</h2>
<ul>
${data.hooksRumors.map(hook => `<li>${hook}</li>`).join('')}
${data.hooksRumors.map(hook => `<li>${escapeHtml(hook)}</li>`).join('')}
</ul>
</div>
` : ''}
@@ -242,7 +257,7 @@ export function dungeonTemplate(data) {
${data.randomEvents.map((event, index) => `
<tr>
<td>${index + 1}</td>
<td>${event}</td>
<td>${escapeHtml(event)}</td>
</tr>
`).join('')}
</tbody>
@@ -255,8 +270,8 @@ export function dungeonTemplate(data) {
<h2>Locations</h2>
${data.rooms.map(room => `
<div class="room">
<h3>${room.name}</h3>
<p>${room.description}</p>
<h3>${escapeHtml(room.name)}</h3>
<p>${escapeHtml(room.description)}</p>
</div>
`).join('')}
</div>
@@ -270,21 +285,20 @@ export function dungeonTemplate(data) {
<table class="encounters-table">
<tbody>
${data.encounters.map((encounter, index) => {
// Truncate details to 2-3 sentences max to prevent overflow
// Truncate details to 4 sentences max to prevent overflow
let details = encounter.details || '';
// Remove location prefix from details if present (format: "Location Name: details")
details = details.replace(/^[^:]+:\s*/, '');
// Split into sentences and keep only first 3
// Keep location prefix in details (format: "Location Name: details")
// Split into sentences and keep only first 4
const sentences = details.match(/[^.!?]+[.!?]+/g) || [details];
if (sentences.length > 3) {
details = sentences.slice(0, 3).join(' ').trim();
if (sentences.length > 4) {
details = sentences.slice(0, 4).join(' ').trim();
}
// Also limit by character count as fallback (max ~250 chars)
if (details.length > 250) {
details = details.substring(0, 247).trim();
// Also limit by character count as fallback (max ~350 chars)
if (details.length > 350) {
details = details.substring(0, 347).trim();
// Try to end at a sentence boundary
const lastPeriod = details.lastIndexOf('.');
if (lastPeriod > 200) {
if (lastPeriod > 280) {
details = details.substring(0, lastPeriod + 1);
} else {
details += '...';
@@ -293,8 +307,8 @@ export function dungeonTemplate(data) {
return `
<tr>
<td>${index + 1}</td>
<td><strong>${encounter.name}</strong></td>
<td>${details}</td>
<td><strong>${escapeHtml(encounter.name)}</strong></td>
<td>${escapeHtml(details)}</td>
</tr>
`;
}).join('')}
@@ -310,7 +324,7 @@ export function dungeonTemplate(data) {
<h2>Treasure</h2>
${data.treasure.map(item => `
<div class="treasure">
${typeof item === 'object' && item.name ? `<strong>${item.name}</strong> — ${item.description}` : item}
${typeof item === 'object' && item.name ? `<strong>${escapeHtml(item.name)}</strong> — ${escapeHtml(item.description)}` : escapeHtml(item)}
</div>
`).join('')}
</div>
@@ -321,7 +335,7 @@ export function dungeonTemplate(data) {
<h2>NPCs</h2>
${data.npcs.map(npc => `
<div class="npc">
<strong>${npc.name}</strong>: ${npc.trait}
<strong>${escapeHtml(npc.name)}</strong>: ${escapeHtml(npc.trait)}
</div>
`).join('')}
</div>
@@ -331,19 +345,19 @@ export function dungeonTemplate(data) {
<div class="section-block">
<h2>Plot Resolutions</h2>
${data.plotResolutions.map(resolution => {
// Truncate to 1-2 sentences max to prevent overflow
// Truncate to 3 sentences max to prevent overflow
let text = resolution || '';
// Split into sentences and keep only first 2
// Split into sentences and keep only first 3
const sentences = text.match(/[^.!?]+[.!?]+/g) || [text];
if (sentences.length > 2) {
text = sentences.slice(0, 2).join(' ').trim();
if (sentences.length > 3) {
text = sentences.slice(0, 3).join(' ').trim();
}
// Also limit by character count as fallback (max ~150 chars)
if (text.length > 150) {
text = text.substring(0, 147).trim();
// Also limit by character count as fallback (max ~200 chars)
if (text.length > 200) {
text = text.substring(0, 197).trim();
// Try to end at a sentence boundary
const lastPeriod = text.lastIndexOf('.');
if (lastPeriod > 100) {
if (lastPeriod > 150) {
text = text.substring(0, lastPeriod + 1);
} else {
text += '...';
@@ -351,7 +365,7 @@ export function dungeonTemplate(data) {
}
return `
<div class="plot-resolution">
${text}
${escapeHtml(text)}
</div>
`;
}).join('')}