function pickRandom(arr) { return arr[Math.floor(Math.random() * arr.length)]; } export function escapeHtml(text) { if (!text) return ''; const map = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }; return String(text).replace(/[&<>"']/g, m => map[m]); } /** Truncate to at most maxSentences, then optionally by maxChars; return immutable result. */ export function truncateText(text, maxSentences, maxChars) { const t = text || ''; const sentences = t.match(/[^.!?]+[.!?]+/g) || [t]; const afterSentences = sentences.length > maxSentences ? sentences.slice(0, maxSentences).join(' ').trim() : t; if (afterSentences.length <= maxChars) return afterSentences; const trimmed = afterSentences.substring(0, maxChars - 3).trim(); const lastPeriod = trimmed.lastIndexOf('.'); return (lastPeriod > maxChars * 0.8 ? trimmed.substring(0, lastPeriod + 1) : trimmed + '...'); } /** Parse event (object or string) into { name, description } with optional truncation. */ export function parseEventForDisplay(event, index) { const pair = typeof event === 'object' && event?.name != null && event?.description != null ? { name: event.name, description: event.description } : typeof event === 'string' ? (() => { const colonMatch = event.match(/^([^:]+):\s*(.+)$/); if (colonMatch) return { name: colonMatch[1].trim(), description: colonMatch[2].trim() }; const words = event.split(/\s+/); return words.length > 3 ? { name: words.slice(0, 2).join(' '), description: words.slice(2).join(' ') } : { name: `Event ${index + 1}`, description: event }; })() : { name: `Event ${index + 1}`, description: String(event || '') }; const description = truncateText(pair.description, 999, 200); return { name: pair.name, description }; } export function dungeonTemplate(data) { const bodyFonts = [ "'Lora', serif", "'Merriweather', serif", "'Libre Baskerville', serif", "'Source Serif 4', serif" ]; const headingFonts = [ "'New Rocker', system-ui", "'UnifrakturCook', cursive", "'IM Fell DW Pica', serif", "'Cinzel', serif", "'Cormorant Garamond', serif", "'Playfair Display', serif" ]; const quoteFonts = [ "'Playfair Display', serif", "'Libre Baskerville', serif", "'Merriweather', serif" ]; const bodyFont = pickRandom(bodyFonts); const headingFont = pickRandom(headingFonts); const quoteFont = pickRandom(quoteFonts); // Check if we have a map image to include const hasMap = data.map && typeof data.map === 'string' && data.map.startsWith('data:image/'); return `
${escapeHtml(data.flavor)}
` : ''}| ${index + 1} | ${escapeHtml(eventName)} | ${escapeHtml(eventDesc)} |
${escapeHtml(desc)}
| ${index + 1} | ${escapeHtml(encounter.name)} | ${escapeHtml(details)} |