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 ` ${data.title}

${escapeHtml(data.title)}

${data.flavor ? `

${escapeHtml(data.flavor)}

` : ''}
${data.hooksRumors && data.hooksRumors.length > 0 ? `

Hooks & Rumors

    ${data.hooksRumors.map(hook => `
  • ${escapeHtml(hook)}
  • `).join('')}
` : ''} ${data.randomEvents && data.randomEvents.length > 0 ? `

Random Events (d6)

${data.randomEvents.map((event, index) => { const { name: eventName, description: eventDesc } = parseEventForDisplay(event, index); return ` `; }).join('')}
${index + 1} ${escapeHtml(eventName)} ${escapeHtml(eventDesc)}
` : ''} ${data.rooms && data.rooms.length > 0 ? `

Locations

${data.rooms.map(room => { const desc = truncateText(room.description || '', 1, 100); return `

${escapeHtml(room.name)}

${escapeHtml(desc)}

`; }).join('')}
` : ''}
${data.encounters && data.encounters.length > 0 ? `

Encounters (d6)

${data.encounters.map((encounter, index) => { const raw = (encounter.details || '').trim(); const withoutName = raw.toLowerCase().startsWith(encounter.name.toLowerCase()) ? raw.substring(encounter.name.length).replace(/^:\s*/, '').trim() : raw; const locationMatch = withoutName.match(/^([^:]+):\s*(.+)$/); const withoutLocation = locationMatch ? (() => { const potential = locationMatch[1].trim(); if (potential.length > 3 && potential.length < 50 && /^[A-Z]/.test(potential)) { return locationMatch[2].trim(); } return withoutName; })() : withoutName; const details = truncateText(withoutLocation, 4, 350); return ` `; }).join('')}
${index + 1} ${escapeHtml(encounter.name)} ${escapeHtml(details)}
` : ''} ${data.treasure && data.treasure.length > 0 ? `

Treasure

${data.treasure.map(item => { if (typeof item === 'object' && item.name && item.description) { return `
${escapeHtml(item.name)} — ${escapeHtml(item.description)}
`; } else if (typeof item === 'string') { // Handle string format "Name — Description" const parts = item.split(/[—–-]/); if (parts.length >= 2) { return `
${escapeHtml(parts[0].trim())} — ${escapeHtml(parts.slice(1).join(' ').trim())}
`; } return `
${escapeHtml(item)}
`; } return ''; }).filter(Boolean).join('')}
` : ''}
${data.npcs && data.npcs.length > 0 ? `

NPCs

${data.npcs.map(npc => { if (typeof npc === 'object' && npc.name && npc.trait) { return `
${escapeHtml(npc.name)}: ${escapeHtml(npc.trait)}
`; } else if (typeof npc === 'string') { // Handle string format "Name: Description" const parts = npc.split(/:/); if (parts.length >= 2) { return `
${escapeHtml(parts[0].trim())}: ${escapeHtml(parts.slice(1).join(':').trim())}
`; } return `
${escapeHtml(npc)}
`; } return ''; }).filter(Boolean).join('')}
` : ''} ${data.plotResolutions && data.plotResolutions.length > 0 ? `

Plot Resolutions

${data.plotResolutions.map(resolution => { const text = truncateText(resolution || '', 1, 120); return `
${escapeHtml(text)}
`; }).join('')}
` : ''}
${hasMap ? `
Dungeon Map
` : ''} `; }