// view.js // Pure view functions /** * Pure view functions for the application. * * Why pure functions returning HTML strings? Because Elm does it, tbh. * - Keeps rendering logic stateless and easy to test. * - Ensures the UI is always a direct function of state, which should in theory totally avoid bugs from incremental DOM updates. * - Using template literals is minimal and browser-native, with no dependencies, and is fun. * * Why escape output? * - Prevents XSS and ensures all user/content data is safely rendered. * * Why semantic/accessible HTML? * - Ensures the app is usable for all users, including those using assistive tech, and is easy to reason about. */ /** * Render the app UI as an HTML string * @param {object} state * @returns {string} */ export function view(state) { return `

Baba Yaga's PokéDex

${state.error ? `` : ''} ${state.pokemon ? renderPokemonResult(state.pokemon) : ''} ${state.pokemon ? '
' : ''} ${state.evolutionChain ? renderEvolutionSection(state) : ''} ${state.evolutionChain ? '
' : ''} ${renderBabaYagaSection(state)}
`; } function renderPokemonResult(pokemon) { return `

${capitalize(pokemon.name)} (#${pokemon.id})

${escape(pokemon.name)} sprite
`; } function renderEvolutionSection(state) { return `

Evolution Chain

${renderEvolutionTree(state.evolutionChain)}
`; } function renderEvolutionTree(evolutionChain) { if (!evolutionChain || !evolutionChain.evolutionChain || !evolutionChain.evolutionChain.chain) { return '

No evolution data available.

'; } const chain = evolutionChain.evolutionChain.chain; let html = '
'; // Render the base species html += `
`; html += `
${capitalize(chain.species.name)}
`; html += `
`; // Render evolution stages if (chain.evolves_to && chain.evolves_to.length > 0) { html += renderEvolutionStages(chain.evolves_to, 1); } html += '
'; return html; } function renderEvolutionStages(evolutions, level) { if (!evolutions || evolutions.length === 0) return ''; let html = `
`; evolutions.forEach((evolution, index) => { html += '
'; // Evolution details if (evolution.evolution_details && evolution.evolution_details.length > 0) { const detail = evolution.evolution_details[0]; html += `
`; html += `${capitalize(detail.trigger.name)}`; if (detail.min_level) { html += ` (Level ${detail.min_level})`; } html += ``; html += `
`; } // Pokemon node html += `
${capitalize(evolution.species.name)}
`; // Recursive evolution stages if (evolution.evolves_to && evolution.evolves_to.length > 0) { html += renderEvolutionStages(evolution.evolves_to, level + 1); } html += '
'; }); html += '
'; return html; } function renderBabaYagaSection(state) { return `

Baba Yaga Data Transformation

${!state.evolutionChain ? '

💡 Search for a Pokémon to automatically load its evolution chain and enable script execution.

' : ''} ${state.evolutionChain && !state.babaYagaScript.trim() ? '

💡 Write a Baba Yaga script or load an example to enable execution.

' : ''}
${state.scriptError ? `` : ''} ${state.scriptOutput ? renderScriptOutput(state.scriptOutput) : ''}
`; } function renderScriptOutput(scriptOutput) { let html = '
'; html += '

Script Results:

'; // Display emitted data if (scriptOutput.emitted && Object.keys(scriptOutput.emitted).length > 0) { html += '
'; html += '
Emitted Data:
'; Object.entries(scriptOutput.emitted).forEach(([event, data]) => { html += `
`; html += `
${escape(event)}:
`; html += `
${escape(JSON.stringify(data, null, 2))}
`; html += `
`; }); html += '
'; } // Display script result if (scriptOutput.result !== undefined) { html += '
'; html += '
Script Result:
'; html += `
${escape(JSON.stringify(scriptOutput.result, null, 2))}
`; html += '
'; } html += '
'; return html; } function escape(str) { /** * Escapes HTML special characters to prevent XSS. * * Why escape here? Keeps all rendering safe by default, so no accidental injection is possible. */ return String(str).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); } function capitalize(str) { return str.charAt(0).toUpperCase() + str.slice(1); }