diff options
Diffstat (limited to 'js/scripting-lang/web')
-rw-r--r-- | js/scripting-lang/web/README-AST.md | 67 | ||||
-rw-r--r-- | js/scripting-lang/web/README.md | 447 | ||||
-rw-r--r-- | js/scripting-lang/web/ast-viewer.html | 150 | ||||
-rw-r--r-- | js/scripting-lang/web/index.html | 23 | ||||
-rw-r--r-- | js/scripting-lang/web/simple.html | 163 | ||||
-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 | ||||
-rw-r--r-- | js/scripting-lang/web/style.css | 306 |
13 files changed, 2308 insertions, 0 deletions
diff --git a/js/scripting-lang/web/README-AST.md b/js/scripting-lang/web/README-AST.md new file mode 100644 index 0000000..194aeec --- /dev/null +++ b/js/scripting-lang/web/README-AST.md @@ -0,0 +1,67 @@ +# Baba Yaga AST Visualizer + +A web-based tool for visualizing the Abstract Syntax Tree (AST) of Baba Yaga code. + +## Features + +- **Real-time AST Generation**: Enter Baba Yaga code and see its AST instantly +- **Token Visualization**: View the tokenized representation of your code +- **Error Display**: Clear error messages for invalid syntax +- **Example Code**: Pre-loaded examples demonstrating different language features +- **Copy to Clipboard**: One-click copying of AST and tokens for easy sharing +- **Clean Interface**: Simple, focused design following the project's design patterns + +## Usage + +1. Open `ast-viewer.html` in your browser +2. Enter Baba Yaga code in the text area +3. Click "Generate AST" or use Ctrl+Enter +4. View the AST and tokens in the output sections below +5. Use the "Copy AST" or "Copy Tokens" buttons to copy the content to your clipboard + +## Examples Included + +- **Simple Assignment**: Basic variable assignment +- **When Expression**: Pattern matching with when/is/then +- **Function Definition**: Arrow function with pattern matching +- **Table Literal**: Creating and accessing table structures +- **Arithmetic Expression**: Mathematical operations and function composition +- **Complex When Expression**: Multi-pattern matching + +## Technical Details + +- Uses the same `lexer.js` and `parser.js` modules as the main language +- No modifications to core language files required +- Pure client-side JavaScript with ES6 modules +- Responsive design that works on desktop and mobile + +## File Structure + +``` +web/ +├── ast.html # Main AST visualization interface +├── src/ +│ └── ast.js # AST generation logic +├── style.css # Shared styling +└── README-AST.md # This file +``` + +## Browser Compatibility + +Requires a modern browser with ES6 module support: +- Chrome 61+ +- Firefox 60+ +- Safari 10.1+ +- Edge 16+ + +## Development + +To run locally: +```bash +cd web +python3 -m http.server 8000 +# or +npx serve . +``` + +Then open `http://localhost:8000/ast.html` \ No newline at end of file diff --git a/js/scripting-lang/web/README.md b/js/scripting-lang/web/README.md new file mode 100644 index 0000000..5c7b1ac --- /dev/null +++ b/js/scripting-lang/web/README.md @@ -0,0 +1,447 @@ +# Baba Yaga's PokeDex + +This application demonstrates how to integrate baba yaga into an interactive web application, and how to use it to perform data transformation and manipulation. + +## Architecture + +### Core TEA Components +- **state.js**: App state definition and helpers +- **update.js**: Pure update function (handles actions/messages) +- **view.js**: Pure view functions (renders HTML as string) +- **app.js**: Entrypoint, main loop, event delegation + +### Baba Yaga Integration +- **api.js**: API fetch logic + Baba Yaga harness integration +- **scripting-harness/**: Baba Yaga FunctionalHarness (which itself includes TEA-inspired state management) +- **lang.js**: Baba Yaga language runtime (imported from parent directory) + +### Data Flow +``` +┌──────────────┐ ┌──────────────┐ ┌──────────────┐ +│ User Input │ ──> │ TEA Actions │ ──> │ API Calls │ +└──────────────┘ └──────────────┘ └──────────────┘ + │ +┌──────────────┐ ┌──────────────┐ ┌─────▼──────┐ +│ UI Update │ <── │ Results │ <── │ Baba Yaga │ +└──────────────┘ └──────────────┘ │ Harness │ + └────────────┘ +``` + +## Pattern + +### TEA Architecture +- **State**: Single immutable state object +- **Update**: Pure function `(state, action) => newState` +- **View**: Pure function `(state) => html` +- **Entrypoint**: Handles events, dispatches actions, triggers re-render + +### Baba Yaga Harness Integration +- **Script Processing**: Pure function `(state) => { model, commands, version }` +- **Command Handling**: Side effects processed by harness adapters +- **State Management**: Automatic versioning and history tracking +- **Error Recovery**: Built-in error handling and recovery mechanisms + +## How to Extend and Use This Template + +### Key Files to Extend +- **src/state.js**: Define the app's state shape and any helper functions for cloning or initializing state. +- **src/update.js**: Add new action/message types and update logic. This is where you handle all state transitions. +- **src/view.js**: Build your UI as a pure function of state. Add new components or views here. +- **src/api.js**: Add or replace API calls as needed for your app's data fetching. **Also contains Baba Yaga integration logic.** +- **src/app.js**: Wire up events, use the generalized `render` function, and add any app-specific logic (e.g., focus management, custom event handling). + +### Using the Generalized `render` Function +The `render` function in `app.js` is designed to be reusable for any app. It takes a config object: + +```js +render({ + root, // DOM element to render into + state, // Current app state + view, // View function: (state) => html + events: [ // Array of event bindings + { selector, event, handler }, + // ... + ], + postRender // Optional: function({ root, state }) for custom logic (e.g., focus) +}); +``` + +## Baba Yaga Language Integration + +### Key Integration Points + +#### **api.js - Baba Yaga Harness Integration** +The `api.js` file contains the core integration logic using the FunctionalHarness: + +```js +// Import the FunctionalHarness +import { FunctionalHarness } from '../../scripting-harness/core/harness.js'; + +// Execute Baba Yaga scripts with the harness +async function executeBabaYagaScript(script, evolutionData) { + // Create harness with the script + const harness = new FunctionalHarness(script, { + logStateChanges: false, + logCommands: false, + debug: false + }); + + // 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, + evolutionData + }; +} +``` + +#### **State Management for Scripts** +The application state includes Baba Yaga-specific fields: + +```js +// In state.js +{ + babaYagaScript: '', // User's script input + scriptOutput: null, // Script execution results + scriptError: null, // Script execution errors + evolutionChain: null, // Data for ..listen operations +} +``` + +The FunctionalHarness provides additional state management features: +- **Versioning**: Automatic state versioning with history +- **Command Processing**: Structured handling of `..emit` operations +- **Error Recovery**: Built-in error handling and recovery mechanisms +- **State Diffing**: Ability to compare state versions + +#### **UI Components** +The view layer includes dedicated components for script editing and execution: + +- **Script Editor**: Textarea for writing Baba Yaga scripts +- **Example Scripts**: Dropdown with pre-built transformation examples +- **Execution Results**: Display of `..emit` output and final results +- **Error Handling**: Clear error messages for script syntax issues + +### Baba Yaga Script Examples + +The application includes several example scripts demonstrating data transformation using the harness pattern: + +```plaintext +/* Basic Evolution Stages */ +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 */ +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 */ +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; +``` + +### State Usage Pattern + +The scripts demonstrate a pattern for working with complex state: + +1. **Capture State**: `state : ..listen;` - Gets the full state object +2. **Extract Substructures**: `chain : state.evolutionChain.chain;` - Extract nested data for easier access +3. **Process Data**: Use the extracted substructures in transformations +4. **Emit Results**: `..emit result;` - Send processed data back + +This pattern helps to avoid deeply nested property access and makes scripts easier to understand. + +### Integration Pattern + +1. **Data Loading**: Fetch external data (Pokémon evolution chains) +2. **Harness Creation**: Create FunctionalHarness instance with Baba Yaga script +3. **Harness Initialization**: Call `await harness.initialize()` (required step) +4. **State Processing**: Use `harness.update()` to execute script with data +5. **Command Extraction**: Extract `..emit` values from `result.commands` +6. **Result Display**: Display transformed results and emitted data +7. **Error Handling**: Leverage harness's built-in error handling and recovery + +This pattern provides a robust, TEA-inspired architecture for embedding Baba Yaga scripts in web applications with proper state management, versioning, and error handling. + +### Key Harness Features Used + +- **State Versioning**: Automatic version tracking with `result.version` +- **Command Processing**: Structured handling of `..emit` operations +- **Error Classification**: Built-in error categorization and recovery +- **Timeout Protection**: Automatic timeout handling for long-running scripts +- **State History**: Access to previous state versions and diffs + +## Versioning Integration Plan + +This application has two separate versioning systems that can be integrated for enhanced debugging and development capabilities: + +### Current Versioning Systems + +#### **Web App Versioning (dev.js)** +- **Purpose**: UI state history for debugging user interactions +- **Features**: + - Step through UI state changes (`dev.next()`, `dev.prev()`) + - Jump to specific states (`dev.goTo(n)`) + - Display history as table (`dev.table()`) + - Console-based debugging interface +- **Scope**: Application state (Pokémon data, script input, UI state) + +#### **Harness Versioning (FunctionalHarness)** +- **Purpose**: Script execution state history for debugging transformations +- **Features**: + - Automatic version tracking for each script execution + - State diffing between versions (`getStateDiff()`) + - Branch creation from specific versions (`createBranch()`) + - Error recovery and rollback capabilities + - Command history tracking +- **Scope**: Script execution state and transformations + +### Integration Opportunities + +#### **1. Unified Versioning Dashboard** +```javascript +// Enhanced dev mode with harness integration +const enhancedDev = { + // Web app versioning + next: () => dev.next(), + prev: () => dev.prev(), + + // Harness versioning + harnessHistory: () => harness.getVersionHistory(), + harnessDiff: (from, to) => harness.getStateDiff(from, to), + + // Combined debugging + scriptExecution: (version) => { + const webState = dev.get(); + const harnessState = harness.stateHistory.getVersion(version); + return { webState, harnessState, diff: harness.getStateDiff(version - 1, version) }; + } +}; +``` + +#### **2. Cross-System State Correlation** +- **Web State → Harness State**: Map UI actions to script execution versions +- **Harness State → Web State**: Track how script results affect UI state +- **Bidirectional Debugging**: Step through both systems simultaneously + +#### **3. Enhanced Debugging Workflow** +```javascript +// Example integration workflow +const debugWorkflow = { + // 1. User performs action (web state changes) + onUserAction: (action) => { + dev.pushState(newState); + console.log(`[Debug] Web state version: ${dev.pointer}`); + }, + + // 2. Script executes (harness state changes) + onScriptExecution: (script, data) => { + const result = await harness.update(data); + console.log(`[Debug] Harness version: ${result.version}`); + console.log(`[Debug] Commands: ${result.commands.length}`); + }, + + // 3. Combined debugging + debugExecution: (webVersion, harnessVersion) => { + const webState = dev.history[webVersion]; + const harnessState = harness.stateHistory.getVersion(harnessVersion); + const diff = harness.getStateDiff(harnessVersion - 1, harnessVersion); + + return { + webState, + harnessState, + scriptDiff: diff, + correlation: `Web v${webVersion} ↔ Harness v${harnessVersion}` + }; + } +}; +``` + +#### **4. Development Tools Enhancement** +```javascript +// Enhanced console API +window.debug = { + // Web app debugging + web: dev, + + // Harness debugging + harness: { + history: () => harness.getVersionHistory(), + diff: (from, to) => harness.getStateDiff(from, to), + branch: (from, name) => harness.createBranch(from, name), + rollback: (version) => harness.rollbackToVersion(version) + }, + + // Combined debugging + combined: { + // Show correlation between web and harness states + correlation: () => { + const webState = dev.get(); + const harnessVersions = harness.getVersionHistory(); + return { webState, harnessVersions }; + }, + + // Step through both systems + step: (direction) => { + if (direction === 'next') { + dev.next(); + // Could also step harness if correlated + } else { + dev.prev(); + } + } + } +}; +``` + +### Implementation Roadmap + +#### **Phase 1: Basic Integration** +- [ ] Extend `dev.js` to expose harness versioning methods +- [ ] Add correlation tracking between web and harness states +- [ ] Create unified console API for both systems + +#### **Phase 2: Enhanced Debugging** +- [ ] Implement bidirectional state stepping +- [ ] Add visual diff display for script transformations +- [ ] Create timeline view showing web ↔ harness correlations + +#### **Phase 3: Advanced Features** +- [ ] Branch management UI for script experimentation +- [ ] State replay capabilities for debugging +- [ ] Performance profiling for script executions + +### Benefits of Integration + +- **Comprehensive Debugging**: Debug both UI interactions and script transformations +- **State Correlation**: Understand how user actions trigger script changes +- **Enhanced Development**: Rich debugging tools for complex data transformations +- **Performance Insights**: Track script execution performance over time +- **Error Recovery**: Leverage harness error recovery in web app context + +## Enhanced Dev Tools Usage + +### Getting Started + +1. **Enable Dev Mode**: Add `?dev=1` to the application URL + ``` + http://localhost:8000/web/?dev=1 + ``` + +2. **Open Console**: Press F12 and navigate to the Console tab + +3. **Test Integration**: Run the automated test suite + ```javascript + testDevTools() + ``` + +### Console API Reference + +#### **Web App Debugging** (`debug.web.*`) +```javascript +// Navigate web state history +debug.web.next() // Step forward +debug.web.prev() // Step backward +debug.web.goTo(n) // Jump to state n +debug.web.get() // Get current state +debug.web.table() // Display as table +debug.web.history // All states array +debug.web.pointer // Current position +``` + +#### **Harness Debugging** (`debug.harness.*`) +```javascript +// Harness versioning and state management +debug.harness.history() // Get version history +debug.harness.diff(from, to) // Compare versions +debug.harness.correlation() // Show correlations +debug.harness.debugExecution(webVer, harnessVer) // Debug execution +``` + +#### **Combined Debugging** (`debug.combined.*`) +```javascript +// Unified debugging operations +debug.combined.correlation() // Current correlation +debug.combined.step('next') // Step both systems +debug.combined.execution(webVer, harnessVer) // Debug execution +``` + +### Example Debugging Session + +```javascript +// 1. Explore web state history +debug.web.table() + +// 2. Check harness versions (after running a script) +debug.harness.history() + +// 3. View correlation between systems +debug.combined.correlation() + +// 4. Compare script execution states +debug.harness.diff(1, 2) + +// 5. Debug specific execution +debug.combined.execution(3, 1) + +// 6. Step through both systems +debug.combined.step('next') +``` + +### Demo Files + +- **`dev-demo.html`**: Comprehensive demo and documentation +- **`test-dev-tools.js`**: Automated test suite for integration verification + +### Troubleshooting + +- **Dev mode not available**: Ensure `?dev=1` is in the URL +- **Harness not available**: Run a Baba Yaga script first to create harness instance +- **Console errors**: Check browser console for detailed error messages + +--- + +Inspired by the [Elm Architecture](https://guide.elm-lang.org/architecture/), but using only browser APIs and ES modules. + +--- + +## MIT License + +Copyright 2025 eli_oat + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/js/scripting-lang/web/ast-viewer.html b/js/scripting-lang/web/ast-viewer.html new file mode 100644 index 0000000..269504f --- /dev/null +++ b/js/scripting-lang/web/ast-viewer.html @@ -0,0 +1,150 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>Baba Yaga AST Viewer</title> + <link rel="stylesheet" href="style.css"> + <style> + textarea { + width: 100%; + min-height: 200px; + padding: 0.6em; + font-size: 1em; + font-family: 'Courier New', monospace; + border: 2px solid var(--color-input-border); + border-radius: 0.2em; + margin-bottom: 1em; + box-sizing: border-box; + resize: vertical; + } + + .output { + white-space: pre-wrap; + font-family: 'Courier New', monospace; + font-size: 0.9em; + background: var(--color-code-bg); + padding: 1em; + border-radius: 0.2em; + border: 1px solid var(--color-result-border); + max-height: 400px; + overflow-y: auto; + resize: vertical; + min-height: 200px; + } + + .error { + color: var(--color-error); + background: #ffeef0; + border-left: 4px solid var(--color-error); + padding: 1em; + margin-bottom: 1em; + } + + .example-selector { + margin-bottom: 1em; + } + + .example-selector select { + padding: 0.6em; + font-size: 1em; + border: 2px solid var(--color-input-border); + border-radius: 0.2em; + background: white; + margin-left: 0.5em; + } + + .example-selector select:focus { + outline: none; + border-color: #007acc; + } + + .output-section { + margin-top: 1.5em; + } + + .output-section h3 { + margin-bottom: 0.5em; + color: var(--color-label); + text-transform: uppercase; + font-size: 0.9em; + } + + .output-container { + position: relative; + } + + .copy-btn { + position: absolute; + top: 0.5em; + right: 0.5em; + background: var(--color-button-bg); + color: var(--color-button-text); + border: none; + border-radius: 0.2em; + padding: 0.3em 0.6em; + font-size: 0.8em; + font-weight: bold; + cursor: pointer; + z-index: 10; + } + + .copy-btn:hover { + background: #005a9e; + } + + .copy-btn:active { + transform: translateY(1px); + } + </style> +</head> +<body> + <main> + <h1>Baba Yaga AST Visualizer</h1> + + <div class="example-selector"> + <label for="examples">Load Example:</label> + <select id="examples"> + <option value="">Choose an example...</option> + <option value="simple">Simple Assignment</option> + <option value="when">When Expression</option> + <option value="function">Function Definition</option> + <option value="table">Table Literal</option> + <option value="arithmetic">Arithmetic Expression</option> + <option value="complex">Complex When Expression</option> + </select> + </div> + + <label for="code-input">Code:</label> + <textarea + id="code-input" + placeholder="Enter Baba Yaga code here... +Example: +x : 42; +result : when x is 42 then "correct" _ then "wrong";" + ></textarea> + + <button id="generate-btn">Generate AST</button> + + <div class="output-section"> + <h3>AST Output:</h3> + <div class="output-container"> + <textarea id="ast-output" class="output" readonly placeholder="AST will appear here..."></textarea> + <button id="copy-ast-btn" class="copy-btn">Copy AST</button> + </div> + </div> + + <div class="output-section"> + <h3>Tokens:</h3> + <div class="output-container"> + <textarea id="tokens-output" class="output" readonly placeholder="Tokens will appear here..."></textarea> + <button id="copy-tokens-btn" class="copy-btn">Copy Tokens</button> + </div> + </div> + + <div id="error-output" class="error" style="display: none;"></div> + </main> + + <script type="module" src="src/ast.js"></script> +</body> +</html> \ No newline at end of file diff --git a/js/scripting-lang/web/index.html b/js/scripting-lang/web/index.html new file mode 100644 index 0000000..1651f44 --- /dev/null +++ b/js/scripting-lang/web/index.html @@ -0,0 +1,23 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>Starter Kit</title> + <link rel="stylesheet" href="style.css"> +</head> +<body> + <main> + <div id="app"></div> + </main> + <script type="module" src="src/app.js"></script> + <script> + // Load dev tools test script if in dev mode + if (window.location.search.includes('dev=1')) { + const script = document.createElement('script'); + script.src = 'test-dev-tools.js'; + document.head.appendChild(script); + } + </script> +</body> +</html> \ No newline at end of file diff --git a/js/scripting-lang/web/simple.html b/js/scripting-lang/web/simple.html new file mode 100644 index 0000000..9b8fd19 --- /dev/null +++ b/js/scripting-lang/web/simple.html @@ -0,0 +1,163 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>Baba Yaga - Simple</title> + <link rel="stylesheet" href="style.css"> + <style> + textarea { + width: 100%; + min-height: 200px; + padding: 0.6em; + font-size: 1em; + font-family: 'Courier New', monospace; + border: 2px solid var(--color-input-border); + border-radius: 0.2em; + margin-bottom: 1em; + box-sizing: border-box; + resize: vertical; + } + + .output { + white-space: pre-wrap; + font-family: 'Courier New', monospace; + font-size: 0.9em; + } + .success { + color: #006600; + } + .error { + color: var(--color-error); + } + .loading { + color: #666; + font-style: italic; + } + </style> +</head> +<body> + <main> + <h1>Baba Yaga</h1> + + <div class="result" id="result" style="display: none;"> + <div class="output" id="output"></div> + </div> + + <label for="script">Script:</label> + <textarea id="script" placeholder="Enter your Baba Yaga code here..."> +factorial : n -> + when n is + 0 then 1 + _ then n * (factorial (n - 1)); + +a : factorial 5; +b : factorial 0; + +..out a; +..out b; + </textarea> + + <button id="run-btn">Run Script</button> + </main> + + <script type="module"> + // Import the Baba Yaga language + import { run } from '../lang.js'; + + const scriptTextarea = document.getElementById('script'); + const runBtn = document.getElementById('run-btn'); + const resultDiv = document.getElementById('result'); + const outputDiv = document.getElementById('output'); + + // Capture console output + let capturedOutput = []; + const originalConsoleLog = console.log; + + function captureConsole() { + capturedOutput = []; + console.log = (...args) => { + // Only capture output that looks like it's from ..out operations + // (single values, not objects or arrays) + const output = args.join(' '); + if (args.length === 1 && typeof args[0] !== 'object') { + capturedOutput.push(output); + } + originalConsoleLog(...args); + }; + } + + function restoreConsole() { + console.log = originalConsoleLog; + } + + // Run script function + async function runScript() { + const script = scriptTextarea.value.trim(); + if (!script) return; + + // Show loading state + resultDiv.style.display = 'block'; + outputDiv.innerHTML = '<span class="loading">Running...</span>'; + runBtn.disabled = true; + + // Capture console output + captureConsole(); + + try { + const result = await run(script); + + // Restore console + restoreConsole(); + + // Build output display + let output = ''; + + // Show captured console output (from ..out operations) + if (capturedOutput.length > 0) { + output += capturedOutput.join('\n'); + } + + // Only show result if there's no output and we have a meaningful result + if (capturedOutput.length === 0 && result !== undefined && result !== null) { + // Try to find the last meaningful result (not the full scope object) + if (typeof result === 'object' && result !== null) { + // If it's an object, look for the last defined variable + const keys = Object.keys(result); + const lastKey = keys[keys.length - 1]; + if (lastKey && lastKey !== 't') { // Skip the 't' table object + output += `Result: ${JSON.stringify(result[lastKey], null, 2)}`; + } + } else { + output += `Result: ${JSON.stringify(result, null, 2)}`; + } + } + + if (output === '') { + output = 'Script executed successfully'; + } + + outputDiv.innerHTML = output; + } catch (error) { + restoreConsole(); + outputDiv.innerHTML = `<span class="error">Error: ${error.message}</span>`; + } finally { + runBtn.disabled = false; + } + } + + // Event listeners + runBtn.addEventListener('click', runScript); + + // Handle Enter key in textarea (Ctrl+Enter to run) + scriptTextarea.addEventListener('keydown', (e) => { + if (e.ctrlKey && e.key === 'Enter') { + e.preventDefault(); + runScript(); + } + }); + + + </script> +</body> +</html> \ No newline at end of file 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 diff --git a/js/scripting-lang/web/style.css b/js/scripting-lang/web/style.css new file mode 100644 index 0000000..4cd5c33 --- /dev/null +++ b/js/scripting-lang/web/style.css @@ -0,0 +1,306 @@ +body { + --color-bg: #f8f8ff; + --color-text: #222; + --color-main-bg: #fff; + --color-main-border: #222; + --color-shadow: #0001; + --color-label: #222; + --color-input-border: #222; + --color-button-bg: #222; + --color-button-text: #fff; + --color-button-disabled-bg: #888; + --color-result-border: #aaa; + --color-result-bg: #f6f6fa; + --color-error: #b30000; + --color-success: #006600; + --color-code-bg: #f0f0f0; + --color-evolution-node: #e8f4f8; + --color-evolution-border: #4a90e2; + + font-family: system-ui, sans-serif; + background: var(--color-bg); + color: var(--color-text); + margin: 0; + padding: 0; +} +.app-header { + max-width: 800px; + margin: 2rem auto 1rem; + display: flex; + justify-content: space-between; + align-items: center; + padding: 0 1.5rem; +} + +.app-header h1 { + margin: 0; + font-size: 1.8rem; +} + +.app-nav { + display: flex; + gap: 1rem; +} + +.nav-link { + color: var(--color-text); + text-decoration: none; + padding: 0.5rem 1rem; + border: 2px solid var(--color-main-border); + border-radius: 6px; + font-weight: 600; + transition: all 0.2s; +} + +.nav-link:hover { + background: var(--color-main-border); + color: var(--color-button-text); +} + +main { + max-width: 800px; + margin: 0 auto 3rem; + background: var(--color-main-bg); + border: 2px solid var(--color-main-border); + border-radius: 8px; + padding: 2rem 1.5rem; + box-shadow: 0 2px 8px var(--color-shadow); +} +label { + font-weight: bold; + text-transform: uppercase; + font-size: 0.98em; + margin-bottom: 0.2em; + display: block; + color: var(--color-label); +} +input[type="text"] { + width: 100%; + padding: 0.6em; + font-size: 1em; + border: 2px solid var(--color-input-border); + border-radius: 0.2em; + margin-bottom: 1em; + box-sizing: border-box; +} +button { + background: var(--color-button-bg); + color: var(--color-button-text); + border: none; + border-radius: 0.2em; + padding: 0.6em 1.2em; + font-weight: bold; + text-transform: uppercase; + cursor: pointer; + font-size: 1em; + margin-bottom: 1em; + margin-right: 0.5em; +} +button:disabled { + background: var(--color-button-disabled-bg); + cursor: not-allowed; +} +.result { + margin-top: 1.5em; + padding: 1em; + border: 1.5px solid var(--color-result-border); + border-radius: 0.3em; + background: var(--color-result-bg); +} +.pokemon-sprite { + width: 120px; + height: 120px; + object-fit: contain; + margin: 0 auto; + display: block; +} +.error { + color: var(--color-error); + font-weight: bold; + margin-top: 1em; +} + +/* Evolution Tree Styles */ +.evolution-section { + margin-top: 2em; + padding: 1em; + border: 1.5px solid var(--color-result-border); + border-radius: 0.3em; + background: var(--color-result-bg); +} + +.evolution-tree { + display: flex; + flex-direction: column; + align-items: center; + gap: 1em; + margin-top: 1em; +} + +.evolution-stage { + display: flex; + justify-content: center; + align-items: center; + gap: 2em; + flex-wrap: wrap; +} + +.evolution-branch { + display: flex; + flex-direction: column; + align-items: center; + gap: 0.5em; +} + +.pokemon-node { + background: var(--color-evolution-node); + border: 2px solid var(--color-evolution-border); + border-radius: 0.5em; + padding: 0.5em 1em; + font-weight: bold; + text-align: center; + min-width: 120px; +} + +.evolution-detail { + font-size: 0.8em; + color: var(--color-text); + text-align: center; +} + +.evolution-method { + background: var(--color-button-bg); + color: var(--color-button-text); + padding: 0.2em 0.5em; + border-radius: 0.3em; + font-size: 0.7em; + text-transform: uppercase; +} + +/* Baba Yaga Section Styles */ +.baba-yaga-section { + margin-top: 2em; + padding: 1em; + border: 1.5px solid var(--color-result-border); + border-radius: 0.3em; + background: var(--color-result-bg); +} + +.script-editor { + margin-top: 1em; +} + +textarea { + width: 100%; + padding: 0.6em; + font-size: 0.9em; + font-family: 'Courier New', monospace; + border: 2px solid var(--color-input-border); + border-radius: 0.2em; + margin-bottom: 1em; + box-sizing: border-box; + resize: vertical; + min-height: 120px; +} + +.script-controls { + display: flex; + gap: 0.5em; + align-items: center; + flex-wrap: wrap; +} + +.help-text { + font-size: 0.9em; + color: #666; + margin-top: 0.5em; + font-style: italic; +} + +.workflow-arrow { + text-align: center; + font-size: 2em; + margin: 1em 0; + color: var(--color-button-bg); + font-weight: bold; +} + +select { + padding: 0.6em; + font-size: 0.9em; + border: 2px solid var(--color-input-border); + border-radius: 0.2em; + background: var(--color-main-bg); + cursor: pointer; +} + +.script-error { + color: var(--color-error); + font-weight: bold; + margin-top: 1em; + padding: 0.5em; + background: #ffe6e6; + border-radius: 0.3em; + border-left: 4px solid var(--color-error); +} + +.script-output { + margin-top: 1em; + padding: 1em; + background: var(--color-code-bg); + border-radius: 0.3em; + border: 1px solid var(--color-result-border); +} + +.script-output h4, .script-output h5, .script-output h6 { + margin-top: 0; + margin-bottom: 0.5em; + color: var(--color-text); +} + +.emitted-data, .script-result { + margin-bottom: 1em; +} + +.emitted-event { + margin-bottom: 1em; + padding: 0.5em; + background: var(--color-main-bg); + border-radius: 0.3em; + border: 1px solid var(--color-result-border); +} + +pre { + background: var(--color-main-bg); + padding: 0.5em; + border-radius: 0.3em; + border: 1px solid var(--color-result-border); + overflow-x: auto; + font-size: 0.8em; + margin: 0; + white-space: pre-wrap; + word-wrap: break-word; +} + +/* Responsive Design */ +@media (max-width: 600px) { + main { + max-width: 95%; + margin: 1rem auto; + padding: 1rem; + } + + .evolution-stage { + flex-direction: column; + gap: 1em; + } + + .script-controls { + flex-direction: column; + align-items: stretch; + } + + button { + margin-right: 0; + } +} \ No newline at end of file |