diff options
Diffstat (limited to 'js/scripting-lang/web/src')
-rw-r--r-- | js/scripting-lang/web/src/api.js | 183 | ||||
-rw-r--r-- | js/scripting-lang/web/src/app.js | 286 | ||||
-rw-r--r-- | js/scripting-lang/web/src/ast.js | 161 | ||||
-rw-r--r-- | js/scripting-lang/web/src/dev.js | 268 | ||||
-rw-r--r-- | js/scripting-lang/web/src/state.js | 18 | ||||
-rw-r--r-- | js/scripting-lang/web/src/update.js | 38 | ||||
-rw-r--r-- | js/scripting-lang/web/src/view.js | 198 |
7 files changed, 1152 insertions, 0 deletions
diff --git a/js/scripting-lang/web/src/api.js b/js/scripting-lang/web/src/api.js new file mode 100644 index 0000000..cf43178 --- /dev/null +++ b/js/scripting-lang/web/src/api.js @@ -0,0 +1,183 @@ +// api.js +// API fetch logic + +/** + * Fetch a Pokémon by name from the PokéAPI + * @param {string} name + * @returns {Promise<object>} Pokémon data + */ +export async function fetchPokemon(name) { + const res = await fetch(`https://pokeapi.co/api/v2/pokemon/${encodeURIComponent(name.toLowerCase())}`); + if (!res.ok) { + throw new Error('Pokémon not found'); + } + return await res.json(); +} + +/** + * Fetch a Pokémon species by name or ID from the PokéAPI + * @param {string|number} nameOrId + * @returns {Promise<object>} Pokémon species data + */ +export async function fetchPokemonSpecies(nameOrId) { + const res = await fetch(`https://pokeapi.co/api/v2/pokemon-species/${encodeURIComponent(nameOrId)}`); + if (!res.ok) { + throw new Error('Pokémon species not found'); + } + return await res.json(); +} + +/** + * Fetch an evolution chain by ID from the PokéAPI + * @param {number} id + * @returns {Promise<object>} Evolution chain data + */ +export async function fetchEvolutionChain(id) { + const res = await fetch(`https://pokeapi.co/api/v2/evolution-chain/${id}`); + if (!res.ok) { + throw new Error('Evolution chain not found'); + } + return await res.json(); +} + +/** + * Get evolution chain ID for a Pokémon + * @param {string|number} pokemonNameOrId + * @returns {Promise<number>} Evolution chain ID + */ +export async function getEvolutionChainId(pokemonNameOrId) { + try { + // First try to get the species data + const species = await fetchPokemonSpecies(pokemonNameOrId); + return species.evolution_chain.url.split('/').slice(-2, -1)[0]; + } catch (error) { + throw new Error(`Could not find evolution chain for ${pokemonNameOrId}: ${error.message}`); + } +} + +/** + * Fetch complete evolution data for a Pokémon + * @param {string|number} pokemonNameOrId + * @returns {Promise<object>} Complete evolution data + */ +export async function fetchEvolutionData(pokemonNameOrId) { + try { + // Get the evolution chain ID + const chainId = await getEvolutionChainId(pokemonNameOrId); + + // Fetch the evolution chain + const evolutionChain = await fetchEvolutionChain(chainId); + + return { + chainId, + evolutionChain, + pokemonName: pokemonNameOrId + }; + } catch (error) { + throw new Error(`Failed to fetch evolution data: ${error.message}`); + } +} + +// Baba Yaga harness integration +import { FunctionalHarness } from '../../scripting-harness/core/harness.js'; + +let harness = null; + +/** + * Initialize Baba Yaga harness + */ +async function initBabaYaga() { + // Harness will be created when we have a script + return true; +} + +/** + * Get the current harness instance for dev mode integration + */ +export function getCurrentHarness() { + console.log('[API] getCurrentHarness called, harness available:', !!harness); + return harness; +} + +/** + * Execute a Baba Yaga script with evolution data using the harness + * @param {string} script - Baba Yaga script to execute + * @param {object} evolutionData - Evolution chain data to work with + * @returns {Promise<object>} Script execution results + */ +export async function executeBabaYagaScript(script, evolutionData) { + try { + // Create harness with the script + harness = new FunctionalHarness(script, { + logStateChanges: false, + logCommands: false, + debug: false + }); + + // IMPORTANT: Initialize the harness before use + await harness.initialize(); + + // Process the evolution data through the harness + const result = await harness.update(evolutionData); + + // Extract emitted values from commands + const emittedValues = result.commands + .filter(cmd => cmd.type === 'emit') + .map(cmd => cmd.value); + + + + return { + result: result.model, + emitted: emittedValues.length > 0 ? emittedValues : {}, + evolutionData + }; + + } catch (error) { + throw new Error(`Baba Yaga script error: ${error.message}`); + } +} + +/** + * Get example Baba Yaga scripts for evolution data + */ +export function getExampleScripts() { + return { + 'Basic Evolution Stages': ` +/* Get evolution stages from the chain */ +state : ..listen; +/* Extract the evolution chain for easier access */ +chain : state.evolutionChain.chain; +getSpeciesName : stage -> stage.species.name; +evolutionStages : map @getSpeciesName chain.evolves_to; +..emit evolutionStages; +`, + 'Evolution Methods': ` +/* Get evolution methods and requirements */ +state : ..listen; +/* Extract the evolution chain for easier access */ +chain : state.evolutionChain.chain; +getEvolutionInfo : evo -> { + species: evo.species.name, + method: evo.evolution_details[0].trigger.name, + level: evo.evolution_details[0].min_level +}; +evolutionMethods : map @getEvolutionInfo chain.evolves_to; +..emit evolutionMethods; +`, + 'Filter by Evolution Method': ` +/* Filter evolutions by method (e.g., level-up only) */ +state : ..listen; +/* Extract the evolution chain for easier access */ +chain : state.evolutionChain.chain; +isLevelUp : evo -> + when evo.evolution_details[0].trigger.name is + "level-up" then true + _ then false; +levelEvolutions : filter @isLevelUp chain.evolves_to; +getSpeciesName : evo -> evo.species.name; +levelEvolutionNames : map @getSpeciesName levelEvolutions; +..emit levelEvolutionNames; +` + }; +} \ No newline at end of file diff --git a/js/scripting-lang/web/src/app.js b/js/scripting-lang/web/src/app.js new file mode 100644 index 0000000..086cba1 --- /dev/null +++ b/js/scripting-lang/web/src/app.js @@ -0,0 +1,286 @@ +// app.js +// Entrypoint for the app + +import { initialState, cloneState } from './state.js'; +import { update } from './update.js'; +import { view } from './view.js'; +import { fetchPokemon, fetchEvolutionData, executeBabaYagaScript, getExampleScripts, getCurrentHarness } from './api.js'; +import { initDevMode } from './dev.js'; + +const root = document.getElementById('app'); +let state = cloneState(initialState); +let dev; + +/** + * Entrypoint for the app. + * + * This file implements a minimal Elm-style architecture using only browser APIs and ES modules. + * - All state is immutable and updated by a pure update function. + * - The entire UI is re-rendered as a string on each state change for simplicity and predictability. + * - Event delegation is used to keep wiring minimal and functional. + * - No 3rd party code: everything is browser-native for cozy portability and clarity. + * + * Why this approach? + * - Functional, pure update/view logic is easier for me to reason about and test. + * - Re-rendering the whole UI avoids bugs from manual DOM updates and keeps state/UI in sync. + * - Minimal code and clear data flow make it easy to extend or adapt for new projects. + */ + +// Enable devMode if ?dev=1 is in the URL +/** + * devMode enables logging of all actions and state transitions for debugging. + * + * Why? This makes the app's state flow transparent, helping you understand and debug the app without extra tooling. + */ +const devMode = window.location.search.includes('dev=1'); + +/** + * Generalized render function for Elm-style apps. + * + * @param {Object} config - Render configuration + * @param {HTMLElement} config.root - Root DOM element + * @param {any} config.state - Current app state + * @param {Function} config.view - View function (state => HTML string) + * @param {Array} [config.events] - Array of { selector, event, handler } + * @param {Function} [config.postRender] - Optional function({ root, state }) for post-render logic + */ +function render({ root, state, view, events = [], postRender }) { + root.innerHTML = view(state); + events.forEach(({ selector, event, handler }) => { + const el = root.querySelector(selector); + if (el) el.addEventListener(event, handler); + }); + if (typeof postRender === 'function') { + postRender({ root, state }); + } +} + +// --- App-specific config for render --- +function postRender({ root, state }) { + // Preserve scroll position + const scrollPosition = window.scrollY; + + const input = root.querySelector('#pokemon-query'); + const error = root.querySelector('.error'); + + // Only handle error focus - don't interfere with user typing + if (error) { + error.focus(); + } else if (input && !document.activeElement) { + // Only auto-focus search input if nothing is currently focused + input.focus(); + input.value = state.query; + input.setSelectionRange(input.value.length, input.value.length); + } + + // Restore scroll position + window.scrollTo(0, scrollPosition); +} + +function doRender() { + render({ + root, + state, + view, + events: [ + { selector: '#search-form', event: 'submit', handler: handleSubmit }, + { selector: '#pokemon-query', event: 'input', handler: handleInput }, + { selector: '#execute-script', event: 'click', handler: handleExecuteScript }, + { selector: '#clear-script', event: 'click', handler: handleClearScript }, + { selector: '#example-scripts', event: 'change', handler: handleLoadExample }, + { selector: '#baba-yaga-script', event: 'input', handler: handleScriptInput }, + ], + postRender, + }); +} + +/** + * Dispatches an action to update state and re-render. + * + * Why centralize dispatch? This enforces a single source of truth for state changes, making the app predictable and easy to debug. + * + * Why log actions/state in devMode? This provides a transparent, time-travel-like view of app logic without needing any extra tooling. + */ +function dispatch(action) { + const prevState = state; + state = update(state, action); + + if (devMode && dev && typeof dev.pushState === 'function') { + dev.pushState(state); + console.groupCollapsed(`Action: ${action.type}`); + console.log('Payload:', action.payload); + console.log('Prev state:', prevState); + console.log('Next state:', state); + console.groupEnd(); + } + + // Only re-render for actions that actually change the UI + const shouldRender = [ + 'FETCH_SUCCESS', + 'FETCH_ERROR', + 'FETCH_EVOLUTION_SUCCESS', + 'FETCH_EVOLUTION_ERROR', + 'EXECUTE_SCRIPT_SUCCESS', + 'EXECUTE_SCRIPT_ERROR', + 'CLEAR_SCRIPT_OUTPUT', + 'UPDATE_BABA_YAGA_SCRIPT' // Only when loading examples + ].includes(action.type); + + if (shouldRender) { + doRender(); + } +} + +/** + * Handles input events by updating state without re-rendering. + */ +function handleInput(e) { + // Update state directly without triggering re-render + state.query = e.target.value; +} + +/** + * Handles script input events by updating state without re-rendering. + */ +function handleScriptInput(e) { + // Update state directly without triggering re-render + state.babaYagaScript = e.target.value; +} + +/** + * Handles form submission, triggers async fetch, and dispatches state updates. + * + * Why handle async here? Keeps update/view pure and centralizes side-effect. + */ +async function handleSubmit(e) { + e.preventDefault(); + if (!state.query.trim()) return; + dispatch({ type: 'FETCH_START' }); + try { + const data = await fetchPokemon(state.query.trim()); + dispatch({ type: 'FETCH_SUCCESS', payload: data }); + + // Automatically fetch evolution chain after successful Pokémon search + try { + dispatch({ type: 'FETCH_EVOLUTION_START' }); + const evolutionData = await fetchEvolutionData(data.name); + dispatch({ type: 'FETCH_EVOLUTION_SUCCESS', payload: evolutionData }); + } catch (evolutionErr) { + dispatch({ type: 'FETCH_EVOLUTION_ERROR', payload: evolutionErr.message }); + } + } catch (err) { + dispatch({ type: 'FETCH_ERROR', payload: err.message }); + } +} + + + +/** + * Handles Baba Yaga script execution. + */ +async function handleExecuteScript(e) { + e.preventDefault(); + if (!state.evolutionChain || !state.babaYagaScript.trim()) return; + + dispatch({ type: 'EXECUTE_SCRIPT_START' }); + try { + // state.evolutionChain contains the wrapper object, pass it directly + const result = await executeBabaYagaScript(state.babaYagaScript, state.evolutionChain); + dispatch({ type: 'EXECUTE_SCRIPT_SUCCESS', payload: result }); + + // Update dev mode with the harness instance for enhanced debugging + console.log('[App] Checking dev mode integration:', { + devMode, + dev: !!dev, + updateHarness: dev ? typeof dev.updateHarness : 'no dev', + updateHarnessValue: dev ? dev.updateHarness : 'no dev' + }); + + if (devMode && dev && typeof dev.updateHarness === 'function') { + const currentHarness = getCurrentHarness(); + console.log('[App] Script executed, current harness:', !!currentHarness); + try { + dev.updateHarness(currentHarness); + console.log('[App] updateHarness called successfully'); + } catch (error) { + console.error('[App] Error calling updateHarness:', error); + } + } else { + console.log('[App] Dev mode or updateHarness not available:', { + devMode, + dev: !!dev, + updateHarness: dev ? typeof dev.updateHarness : false + }); + + // Try to access the function directly from window.dev + if (window.dev && typeof window.dev.updateHarness === 'function') { + console.log('[App] Found updateHarness on window.dev, trying direct call'); + const currentHarness = getCurrentHarness(); + try { + window.dev.updateHarness(currentHarness); + console.log('[App] Direct updateHarness call successful'); + } catch (error) { + console.error('[App] Error in direct updateHarness call:', error); + } + } + } + } catch (err) { + dispatch({ type: 'EXECUTE_SCRIPT_ERROR', payload: err.message }); + } +} + +/** + * Handles script clearing. + */ +function handleClearScript(e) { + e.preventDefault(); + dispatch({ type: 'UPDATE_BABA_YAGA_SCRIPT', payload: '' }); + dispatch({ type: 'CLEAR_SCRIPT_OUTPUT' }); +} + +/** + * Handles loading example scripts. + */ +function handleLoadExample(e) { + const selectedExample = e.target.value; + if (!selectedExample) return; + + const examples = getExampleScripts(); + if (examples[selectedExample]) { + // Update state and trigger re-render to show the example + state.babaYagaScript = examples[selectedExample]; + doRender(); + } + + // Reset the select + e.target.value = ''; +} + +// Initialize dev mode before first render +if (devMode) { + dev = initDevMode({ + getState: () => state, + setState: s => { state = s; }, + render: doRender, + harness: null // Will be updated when harness is created + }); +} + +// Initial render +doRender(); + +function updateHistoryInfo() { + if (!devMode || !dev) return; + dev.update(); +} + +function setHistoryPointer(idx) { + const info = dev.getHistoryInfo(); + if (idx < 1 || idx > info.length) return; + const newState = dev.setPointer(idx - 1); + if (newState) { + state = newState; + doRender(); + updateHistoryInfo(); + } +} diff --git a/js/scripting-lang/web/src/ast.js b/js/scripting-lang/web/src/ast.js new file mode 100644 index 0000000..522d026 --- /dev/null +++ b/js/scripting-lang/web/src/ast.js @@ -0,0 +1,161 @@ +// ast.js +// AST visualization tool for Baba Yaga language + +import { lexer, parser } from '../../lang.js'; + +const examples = { + simple: `x : 42;`, + + when: `result : when x is 42 then "correct" _ then "wrong";`, + + function: `factorial : n -> + when n is + 0 then 1 + _ then n * (factorial (n - 1));`, + + table: `person : {name: "Baba Yaga", age: 99, active: true}; +numbers : {1, 2, 3, 4, 5};`, + + arithmetic: `result : 5 + 3 * 2; +composed : compose @double @increment 5;`, + + complex: `classify : x y -> + when x y is + 0 0 then "both zero" + 0 _ then "x is zero" + _ 0 then "y is zero" + _ _ then "neither zero";` +}; + +// DOM elements - will be initialized when DOM is ready +let codeInput, generateBtn, examplesSelect, astOutput, tokensOutput, errorOutput, copyAstBtn, copyTokensBtn; + +// Initialize when DOM is ready +document.addEventListener('DOMContentLoaded', () => { + // Initialize DOM elements + codeInput = document.getElementById('code-input'); + generateBtn = document.getElementById('generate-btn'); + examplesSelect = document.getElementById('examples'); + astOutput = document.getElementById('ast-output'); + tokensOutput = document.getElementById('tokens-output'); + errorOutput = document.getElementById('error-output'); + copyAstBtn = document.getElementById('copy-ast-btn'); + copyTokensBtn = document.getElementById('copy-tokens-btn'); + + // Example selector functionality + examplesSelect.addEventListener('change', () => { + const selectedExample = examplesSelect.value; + if (selectedExample && examples[selectedExample]) { + codeInput.value = examples[selectedExample]; + generateAST(); + } + }); + + // Generate button click handler + generateBtn.addEventListener('click', generateAST); + + // Copy button click handlers + copyAstBtn.addEventListener('click', () => copyToClipboard(astOutput, 'AST')); + copyTokensBtn.addEventListener('click', () => copyToClipboard(tokensOutput, 'Tokens')); + + // Auto-generate on Enter key (but not in textarea) + document.addEventListener('keydown', (e) => { + if (e.key === 'Enter' && e.ctrlKey && document.activeElement !== codeInput) { + generateAST(); + } + }); + + // Initialize with a default example + codeInput.value = examples.when; + generateAST(); +}); + +// Generate AST from code +function generateAST() { + if (!codeInput) return; // DOM not ready yet + + const code = codeInput.value.trim(); + + if (!code) { + showError('Please enter some code to analyze.'); + return; + } + + try { + // Generate tokens + const tokens = lexer(code); + showTokens(tokens); + + // Generate AST + const ast = parser(tokens); + showAST(ast); + + // Clear any previous errors + showError(''); + + } catch (error) { + showError(`Parsing Error: ${error.message}`); + showAST(null); + showTokens(null); + } +} + +// Display AST in formatted JSON +function showAST(ast) { + if (!astOutput) return; // DOM not ready yet + + if (ast) { + astOutput.value = JSON.stringify(ast, null, 2); + } else { + astOutput.value = 'No AST available due to parsing error.'; + } +} + +// Display tokens in formatted JSON +function showTokens(tokens) { + if (!tokensOutput) return; // DOM not ready yet + + if (tokens) { + tokensOutput.value = JSON.stringify(tokens, null, 2); + } else { + tokensOutput.value = 'No tokens available due to parsing error.'; + } +} + +// Display error message +function showError(message) { + if (!errorOutput) return; // DOM not ready yet + + if (message) { + errorOutput.textContent = message; + errorOutput.style.display = 'block'; + } else { + errorOutput.style.display = 'none'; + } +} + +// Copy text to clipboard +async function copyToClipboard(textarea, label) { + if (!textarea || !textarea.value) { + showError(`No ${label} content to copy.`); + return; + } + + try { + await navigator.clipboard.writeText(textarea.value); + + // Show temporary success message + const originalText = errorOutput.textContent; + showError(`${label} copied to clipboard!`); + + // Clear success message after 2 seconds + setTimeout(() => { + if (errorOutput.textContent === `${label} copied to clipboard!`) { + showError(''); + } + }, 2000); + + } catch (error) { + showError(`Failed to copy ${label}: ${error.message}`); + } +} \ No newline at end of file diff --git a/js/scripting-lang/web/src/dev.js b/js/scripting-lang/web/src/dev.js new file mode 100644 index 0000000..8341d1c --- /dev/null +++ b/js/scripting-lang/web/src/dev.js @@ -0,0 +1,268 @@ +// devMode.js +// Enhanced dev mode with harness integration for unified debugging + +/** + * Initialize enhanced dev mode: exposes an API for stepping through state history + * with integration to Baba Yaga harness versioning capabilities. + * @param {object} opts + * @param {function} opts.getState - returns current app state + * @param {function} opts.setState - sets app state + * @param {function} opts.render - triggers app re-render + * @param {object} opts.harness - Baba Yaga FunctionalHarness instance (optional) + */ +export function initDevMode({ getState, setState, render, harness = null }) { + let history = []; + let pointer = -1; + let firstLoad = true; + let harnessCorrelation = []; // Track web state ↔ harness state correlation + + function pushState(state) { + if (pointer < history.length - 1) history = history.slice(0, pointer + 1); + history.push(clone(state)); + pointer = history.length - 1; + + // Track correlation with harness if available + if (harness) { + const harnessVersion = harness.currentVersion || 0; + harnessCorrelation.push({ + webVersion: pointer, + harnessVersion, + timestamp: Date.now() + }); + } + + logInstructions(); + } + + function goTo(idx) { + if (idx < 0 || idx >= history.length) return; + pointer = idx; + setState(clone(history[pointer])); + render(); + logInstructions(); + } + + function next() { + if (pointer < history.length - 1) goTo(pointer + 1); + } + + function prev() { + if (pointer > 0) goTo(pointer - 1); + } + + function get() { + return history[pointer]; + } + + function clone(obj) { + return JSON.parse(JSON.stringify(obj)); + } + + function table(obj) { + console.table(dev.history); + } + + // Harness integration functions + function getHarnessHistory() { + console.log('[DevMode] getHarnessHistory called, harness available:', !!harness); + if (!harness) { + console.warn('[DevMode] No harness available for versioning - run a Baba Yaga script first'); + return []; + } + const history = harness.getVersionHistory(); + console.log('[DevMode] Harness history:', history.length, 'versions'); + return history; + } + + function getHarnessDiff(from, to) { + if (!harness) { + console.warn('[DevMode] No harness available for diffing'); + return null; + } + return harness.getStateDiff(from, to); + } + + function getCorrelation() { + console.log('[DevMode] getCorrelation called, harness available:', !!harness); + if (!harness) { + console.warn('[DevMode] No harness available for correlation - run a Baba Yaga script first'); + return null; + } + + const webState = get(); + const harnessVersions = getHarnessHistory(); + const currentCorrelation = harnessCorrelation.find(c => c.webVersion === pointer); + + const result = { + webState, + webVersion: pointer, + harnessVersions, + currentCorrelation, + allCorrelations: harnessCorrelation + }; + + console.log('[DevMode] Correlation result:', { + webVersion: result.webVersion, + harnessVersions: result.harnessVersions.length, + correlations: result.allCorrelations.length + }); + + return result; + } + + function debugExecution(webVersion, harnessVersion) { + if (!harness) { + console.warn('[DevMode] No harness available for execution debugging'); + return null; + } + + const webState = history[webVersion]; + const harnessState = harness.stateHistory.getVersion(harnessVersion); + const diff = getHarnessDiff(harnessVersion - 1, harnessVersion); + + return { + webState, + harnessState, + scriptDiff: diff, + correlation: `Web v${webVersion} ↔ Harness v${harnessVersion}` + }; + } + + function stepCombined(direction) { + if (direction === 'next') { + next(); + // Could also step harness if correlated + const correlation = harnessCorrelation.find(c => c.webVersion === pointer); + if (correlation && harness) { + console.log(`[DevMode] Web v${pointer} correlates with Harness v${correlation.harnessVersion}`); + } + } else { + prev(); + } + } + + function logInstructions() { + if (firstLoad) { + console.log('[DevMode] Enhanced state history debugger with harness integration'); + console.log('Web App Debugging:'); + console.log('- dev.next() // step forward'); + console.log('- dev.prev() // step backward'); + console.log('- dev.goTo(n) // jump to state n (1-based)'); + console.log('- dev.get() // get current state'); + console.log('- dev.table() // display history as a table'); + console.log('- dev.history // array of all states'); + console.log('- dev.pointer // current pointer (0-based)'); + + if (harness) { + console.log('\nHarness Integration:'); + console.log('- dev.harnessHistory() // get harness version history'); + console.log('- dev.harnessDiff(from, to) // get state diff'); + console.log('- dev.correlation() // show web ↔ harness correlation'); + console.log('- dev.debugExecution(webVer, harnessVer) // debug specific execution'); + console.log('- dev.stepCombined(direction) // step both systems'); + } + + console.log('\nEnhanced Console API:'); + console.log('- debug.web // web app debugging'); + console.log('- debug.harness // harness debugging'); + console.log('- debug.combined // combined debugging'); + + firstLoad = false; + } + } + + // Function to update harness instance (called after script execution) + function updateHarness(newHarness) { + console.log('[DevMode] updateHarness called with:', !!newHarness); + harness = newHarness; + console.log('[DevMode] Harness instance updated for enhanced debugging'); + + // Re-expose the enhanced debug API with updated harness + window.debug = enhancedDebug; + } + + // Function to check current dev tools status + function getStatus() { + return { + devMode: true, + webStates: history.length, + currentPointer: pointer, + harnessAvailable: !!harness, + harnessVersions: harness ? harness.getVersionHistory().length : 0, + correlations: harnessCorrelation.length + }; + } + + // Enhanced console API + const enhancedDebug = { + // Web app debugging + web: { + next, + prev, + goTo, + get, + table, + get pointer() { return pointer; }, + get history() { return history.slice(); }, + }, + + // Harness debugging + harness: { + history: getHarnessHistory, + diff: getHarnessDiff, + correlation: getCorrelation, + debugExecution, + }, + + // Combined debugging + combined: { + correlation: getCorrelation, + step: stepCombined, + execution: debugExecution, + status: getStatus, + } + }; + + // Expose API globally for console use + window.dev = { + next, + prev, + goTo, + get, + table, + get pointer() { return pointer; }, + get history() { return history.slice(); }, + // Harness integration methods + harnessHistory: getHarnessHistory, + harnessDiff: getHarnessDiff, + correlation: getCorrelation, + debugExecution, + stepCombined, + updateHarness, + getStatus, + }; + + // Debug logging to verify function exposure + console.log('[DevMode] Dev API functions exposed:', { + updateHarness: typeof window.dev.updateHarness, + getStatus: typeof window.dev.getStatus, + harnessHistory: typeof window.dev.harnessHistory, + correlation: typeof window.dev.correlation + }); + + // Expose enhanced debug API + window.debug = enhancedDebug; + + // Debug logging to verify API exposure + console.log('[DevMode] Enhanced debug API exposed:', { + debugAvailable: typeof window.debug !== 'undefined', + webAvailable: typeof window.debug?.web !== 'undefined', + harnessAvailable: typeof window.debug?.harness !== 'undefined', + combinedAvailable: typeof window.debug?.combined !== 'undefined' + }); + + // Initial state + pushState(getState()); + + return { pushState }; +} \ No newline at end of file diff --git a/js/scripting-lang/web/src/state.js b/js/scripting-lang/web/src/state.js new file mode 100644 index 0000000..0bfada6 --- /dev/null +++ b/js/scripting-lang/web/src/state.js @@ -0,0 +1,18 @@ +// state.js +// App state definition and helpers + +export const initialState = { + query: '', + pokemon: null, + evolutionChain: null, + evolutionData: null, // Transformed by Baba Yaga + babaYagaScript: '', // User's transformation script + scriptOutput: null, // Results from Baba Yaga execution + scriptError: null, // Baba Yaga script execution errors + loading: false, + error: null +}; + +export function cloneState(state) { + return JSON.parse(JSON.stringify(state)); +} \ No newline at end of file diff --git a/js/scripting-lang/web/src/update.js b/js/scripting-lang/web/src/update.js new file mode 100644 index 0000000..e13656e --- /dev/null +++ b/js/scripting-lang/web/src/update.js @@ -0,0 +1,38 @@ +// update.js +// Pure update function + +/** + * @param {object} state - Current state + * @param {object} action - { type, payload } + * @returns {object} new state + */ +export function update(state, action) { + switch (action.type) { + case 'UPDATE_QUERY': + return { ...state, query: action.payload, error: null }; + case 'FETCH_START': + return { ...state, loading: true, error: null, pokemon: null, evolutionChain: null }; + case 'FETCH_SUCCESS': + return { ...state, loading: false, error: null, pokemon: action.payload }; + case 'FETCH_ERROR': + return { ...state, loading: false, error: action.payload, pokemon: null }; + case 'FETCH_EVOLUTION_START': + return { ...state, loading: true, error: null, evolutionChain: null }; + case 'FETCH_EVOLUTION_SUCCESS': + return { ...state, loading: false, error: null, evolutionChain: action.payload }; + case 'FETCH_EVOLUTION_ERROR': + return { ...state, loading: false, error: action.payload, evolutionChain: null }; + case 'UPDATE_BABA_YAGA_SCRIPT': + return { ...state, babaYagaScript: action.payload, scriptError: null }; + case 'EXECUTE_SCRIPT_START': + return { ...state, scriptError: null, scriptOutput: null }; + case 'EXECUTE_SCRIPT_SUCCESS': + return { ...state, scriptOutput: action.payload, scriptError: null }; + case 'EXECUTE_SCRIPT_ERROR': + return { ...state, scriptError: action.payload, scriptOutput: null }; + case 'CLEAR_SCRIPT_OUTPUT': + return { ...state, scriptOutput: null, scriptError: null }; + default: + return state; + } +} \ No newline at end of file diff --git a/js/scripting-lang/web/src/view.js b/js/scripting-lang/web/src/view.js new file mode 100644 index 0000000..ab64910 --- /dev/null +++ b/js/scripting-lang/web/src/view.js @@ -0,0 +1,198 @@ +// 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 ` + <header class="app-header"> + <h1>Baba Yaga's PokéDex</h1> + </header> + <container> + <form id="search-form" autocomplete="off"> + <label for="pokemon-query">Pokémon Name (or number)</label> + <input id="pokemon-query" type="text" value="${escape(state.query)}" placeholder="e.g. eevee" aria-label="Pokémon Name" required /> + <button type="submit" ${state.loading ? 'disabled' : ''}>${state.loading ? 'Loading...' : 'Search'}</button> + </form> + ${state.error ? `<div class="error" role="alert" tabindex="-1">${escape(state.error)}</div>` : ''} + + ${state.pokemon ? renderPokemonResult(state.pokemon) : ''} + ${state.pokemon ? '<div class="workflow-arrow">⬇</div>' : ''} + ${state.evolutionChain ? renderEvolutionSection(state) : ''} + ${state.evolutionChain ? '<div class="workflow-arrow">⬇</div>' : ''} + ${renderBabaYagaSection(state)} + </container> + `; +} + +function renderPokemonResult(pokemon) { + return ` + <div class="result"> + <h2>${capitalize(pokemon.name)} (#${pokemon.id})</h2> + <img class="pokemon-sprite" src="${pokemon.sprites.front_default}" alt="${escape(pokemon.name)} sprite" /> + <ul> + <li>Type: <b>${pokemon.types.map(t => capitalize(escape(t.type.name))).join(', ')}</b></li> + <li>Height: <b>${pokemon.height / 10} m</b></li> + <li>Weight: <b>${pokemon.weight / 10} kg</b></li> + </ul> + </div> + `; +} + +function renderEvolutionSection(state) { + return ` + <div class="evolution-section"> + <h3>Evolution Chain</h3> + ${renderEvolutionTree(state.evolutionChain)} + </div> + `; +} + +function renderEvolutionTree(evolutionChain) { + if (!evolutionChain || !evolutionChain.evolutionChain || !evolutionChain.evolutionChain.chain) { + return '<p>No evolution data available.</p>'; + } + + const chain = evolutionChain.evolutionChain.chain; + let html = '<div class="evolution-tree">'; + + // Render the base species + html += `<div class="evolution-stage base">`; + html += `<div class="pokemon-node">${capitalize(chain.species.name)}</div>`; + html += `</div>`; + + // Render evolution stages + if (chain.evolves_to && chain.evolves_to.length > 0) { + html += renderEvolutionStages(chain.evolves_to, 1); + } + + html += '</div>'; + return html; +} + +function renderEvolutionStages(evolutions, level) { + if (!evolutions || evolutions.length === 0) return ''; + + let html = `<div class="evolution-stage level-${level}">`; + + evolutions.forEach((evolution, index) => { + html += '<div class="evolution-branch">'; + + // Evolution details + if (evolution.evolution_details && evolution.evolution_details.length > 0) { + const detail = evolution.evolution_details[0]; + html += `<div class="evolution-detail">`; + html += `<span class="evolution-method">${capitalize(detail.trigger.name)}`; + if (detail.min_level) { + html += ` (Level ${detail.min_level})`; + } + html += `</span>`; + html += `</div>`; + } + + // Pokemon node + html += `<div class="pokemon-node">${capitalize(evolution.species.name)}</div>`; + + // Recursive evolution stages + if (evolution.evolves_to && evolution.evolves_to.length > 0) { + html += renderEvolutionStages(evolution.evolves_to, level + 1); + } + + html += '</div>'; + }); + + html += '</div>'; + return html; +} + +function renderBabaYagaSection(state) { + return ` + <div class="baba-yaga-section"> + <h3>Baba Yaga Data Transformation</h3> + <div class="script-editor"> + <label for="baba-yaga-script">Transformation Script:</label> + <textarea + id="baba-yaga-script" + placeholder="Write your Baba Yaga script here..." + rows="8" + >${escape(state.babaYagaScript)}</textarea> + <div class="script-controls"> + <button id="execute-script" type="button" ${!state.evolutionChain || !state.babaYagaScript.trim() ? 'disabled' : ''}> + Execute Script + </button> + <button id="clear-script" type="button">Clear</button> + <select id="example-scripts"> + <option value="">Load Example...</option> + <option value="Basic Evolution Stages">Basic Evolution Stages</option> + <option value="Evolution Methods">Evolution Methods</option> + <option value="Filter by Evolution Method">Filter by Evolution Method</option> + </select> + </div> + ${!state.evolutionChain ? '<p class="help-text">💡 Search for a Pokémon to automatically load its evolution chain and enable script execution.</p>' : ''} + ${state.evolutionChain && !state.babaYagaScript.trim() ? '<p class="help-text">💡 Write a Baba Yaga script or load an example to enable execution.</p>' : ''} + </div> + + ${state.scriptError ? `<div class="script-error" role="alert">${escape(state.scriptError)}</div>` : ''} + ${state.scriptOutput ? renderScriptOutput(state.scriptOutput) : ''} + </div> + `; +} + +function renderScriptOutput(scriptOutput) { + let html = '<div class="script-output">'; + html += '<h4>Script Results:</h4>'; + + // Display emitted data + if (scriptOutput.emitted && Object.keys(scriptOutput.emitted).length > 0) { + html += '<div class="emitted-data">'; + html += '<h5>Emitted Data:</h5>'; + Object.entries(scriptOutput.emitted).forEach(([event, data]) => { + html += `<div class="emitted-event">`; + html += `<h6>${escape(event)}:</h6>`; + html += `<pre>${escape(JSON.stringify(data, null, 2))}</pre>`; + html += `</div>`; + }); + html += '</div>'; + } + + // Display script result + if (scriptOutput.result !== undefined) { + html += '<div class="script-result">'; + html += '<h5>Script Result:</h5>'; + html += `<pre>${escape(JSON.stringify(scriptOutput.result, null, 2))}</pre>`; + html += '</div>'; + } + + html += '</div>'; + 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); +} \ No newline at end of file |