diff options
Diffstat (limited to 'js/baba-yaga/web')
-rw-r--r-- | js/baba-yaga/web/app.js | 497 | ||||
-rw-r--r-- | js/baba-yaga/web/editor/README.md | 210 | ||||
-rw-r--r-- | js/baba-yaga/web/editor/debug-modules.html | 107 | ||||
-rw-r--r-- | js/baba-yaga/web/editor/index.html | 577 | ||||
-rw-r--r-- | js/baba-yaga/web/editor/js/ast-synchronizer.js | 463 | ||||
-rw-r--r-- | js/baba-yaga/web/editor/js/baba-yaga-mode.js | 191 | ||||
-rw-r--r-- | js/baba-yaga/web/editor/js/baba-yaga-runner.js | 564 | ||||
-rw-r--r-- | js/baba-yaga/web/editor/js/editor.js | 1004 | ||||
-rw-r--r-- | js/baba-yaga/web/editor/js/formatter.js | 621 | ||||
-rw-r--r-- | js/baba-yaga/web/editor/js/main.js | 225 | ||||
-rw-r--r-- | js/baba-yaga/web/editor/js/structural-editors.js | 501 | ||||
-rw-r--r-- | js/baba-yaga/web/editor/js/tree-sitter-baba-yaga.js | 79 | ||||
-rw-r--r-- | js/baba-yaga/web/editor/structural.html | 101 | ||||
-rw-r--r-- | js/baba-yaga/web/editor/styles.css | 755 | ||||
-rw-r--r-- | js/baba-yaga/web/editor/test-formatter.html | 155 | ||||
-rw-r--r-- | js/baba-yaga/web/editor/test-integration.html | 109 | ||||
-rw-r--r-- | js/baba-yaga/web/index.html | 355 |
17 files changed, 6514 insertions, 0 deletions
diff --git a/js/baba-yaga/web/app.js b/js/baba-yaga/web/app.js new file mode 100644 index 0000000..ad92716 --- /dev/null +++ b/js/baba-yaga/web/app.js @@ -0,0 +1,497 @@ +import { createLexer } from '../lexer.js'; +import { createParser } from '../parser.js'; +import { createInterpreter } from '../interpreter.js'; + +/** + * Baba Yaga REPL Application + * A mobile-first, accessible CLI-style REPL for the Baba Yaga language + */ + +class BabaYagaREPL { + constructor() { + this.history = []; + this.historyIndex = -1; + this.sessionCode = ''; + + // DOM elements + this.messagesEl = document.getElementById('messages'); + this.inputEl = document.getElementById('input'); + this.sendBtn = document.getElementById('send'); + this.clearBtn = document.getElementById('clear'); + this.loadBtn = document.getElementById('load'); + this.fileInputEl = document.getElementById('fileInput'); + this.examplesBtn = document.getElementById('examples'); + this.helpBtn = document.getElementById('help'); + + this.setupEventListeners(); + this.showWelcome(); + } + + setupEventListeners() { + // Execute code + this.sendBtn.addEventListener('click', () => this.executeCode()); + this.inputEl.addEventListener('keydown', (e) => { + if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) { + e.preventDefault(); + this.executeCode(); + } else if (e.key === 'ArrowUp') { + e.preventDefault(); + this.navigateHistory(-1); + } else if (e.key === 'ArrowDown') { + e.preventDefault(); + this.navigateHistory(1); + } + }); + + // Auto-resize textarea + this.inputEl.addEventListener('input', () => this.autoResize()); + + // Clear output + this.clearBtn.addEventListener('click', () => this.clearOutput()); + + // Load file + this.loadBtn.addEventListener('click', () => this.fileInputEl.click()); + this.fileInputEl.addEventListener('change', (e) => this.handleFileLoad(e)); + + // Load examples + this.examplesBtn.addEventListener('click', () => this.loadExamples()); + + // Help + this.helpBtn.addEventListener('click', () => this.showHelp()); + + // Focus management + this.inputEl.focus(); + } + + showWelcome() { + this.addMessage('Baba Yaga REPL', 'info'); + this.addMessage('Type /help for available commands', 'info'); + } + + executeCode() { + const code = this.inputEl.value.trim(); + if (!code) return; + + // Add to history + this.addToHistory(code); + + // Display input + this.addInputMessage(code); + + try { + // Check for slash commands + if (code.startsWith('/')) { + this.handleSlashCommand(code); + this.clearInput(); + return; + } + + // Execute Baba Yaga code with session persistence + const result = this.evaluateWithSession(code); + + if (result.ok) { + if (result.value !== undefined) { + this.addMessage(this.formatValue(result.value), 'output'); + } + } else { + this.addMessage(`Error: ${result.error.message}`, 'error'); + if (result.error.codeFrame) { + this.addMessage(result.error.codeFrame, 'error'); + } + } + } catch (error) { + this.addMessage(`Unexpected error: ${error.message}`, 'error'); + } + + this.clearInput(); + } + + evaluate(source) { + try { + const lexer = createLexer(source); + const tokens = lexer.allTokens(); + const parser = createParser(tokens); + const ast = parser.parse(); + + const host = { + io: { + out: (...args) => { + const text = args.map(arg => this.formatValue(arg)).join(' '); + this.addMessage(text, 'output'); + }, + in: () => { + // For now, return empty string - could be enhanced with stdin + return ''; + } + } + }; + + const interpreter = createInterpreter(ast, host); + const value = interpreter.interpret(); + + return { ok: true, value }; + } catch (error) { + const message = error?.message || String(error); + const lineMatch = / at (\d+):(\d+)/.exec(message); + const line = lineMatch ? Number(lineMatch[1]) : null; + const column = lineMatch ? Number(lineMatch[2]) : null; + const codeFrame = this.makeCodeFrame(source, line, column); + + return { + ok: false, + error: { + message, + line, + column, + codeFrame + } + }; + } + } + + evaluateWithSession(source) { + try { + // Combine session code with new code + const fullSource = this.sessionCode + source; + + const lexer = createLexer(fullSource); + const tokens = lexer.allTokens(); + const parser = createParser(tokens); + const ast = parser.parse(); + + const host = { + io: { + out: (...args) => { + const text = args.map(arg => this.formatValue(arg)).join(' '); + this.addMessage(text, 'output'); + }, + in: () => { + // For now, return empty string - could be enhanced with stdin + return ''; + } + } + }; + + const interpreter = createInterpreter(ast, host); + const value = interpreter.interpret(); + + // If successful, add to session code + this.sessionCode += source + '\n'; + + return { ok: true, value }; + } catch (error) { + const message = error?.message || String(error); + const lineMatch = / at (\d+):(\d+)/.exec(message); + const line = lineMatch ? Number(lineMatch[1]) : null; + const column = lineMatch ? Number(lineMatch[2]) : null; + const codeFrame = this.makeCodeFrame(source, line, column); + + return { + ok: false, + error: { + message, + line, + column, + codeFrame + } + }; + } + } + + formatValue(value) { + if (value === null || value === undefined) { + return 'null'; + } + + if (typeof value === 'number') { + return value.toString(); + } + + if (typeof value === 'string') { + return value; + } + + if (Array.isArray(value)) { + return JSON.stringify(value); + } + + if (value && typeof value === 'object') { + if (value.type === 'Object' && value.properties) { + const obj = {}; + for (const [key, val] of value.properties.entries()) { + obj[key] = this.formatValue(val); + } + return JSON.stringify(obj, null, 2); + } + + if (value.value !== undefined) { + return value.value.toString(); + } + + // Handle native functions - don't show the function object + if (value.type === 'NativeFunction') { + return ''; // Return empty string for native functions + } + + // Handle function objects - don't show the function definition + if (value.type === 'Function') { + return ''; // Return empty string for function objects + } + } + + return JSON.stringify(value, null, 2); + } + + makeCodeFrame(source, line, column) { + if (!line || !column) return ''; + + const lines = source.split(/\r?\n/); + const idx = line - 1; + const context = [idx - 1, idx, idx + 1].filter(i => i >= 0 && i < lines.length); + const pad = n => String(n + 1).padStart(4, ' '); + const caret = ' '.repeat(column - 1) + '^'; + const rows = context.map(i => `${pad(i)} | ${lines[i]}`); + rows.splice(context.indexOf(idx) + 1, 0, ` | ${caret}`); + return rows.join('\n'); + } + + addMessage(text, type = 'output') { + const messageDiv = document.createElement('div'); + messageDiv.className = 'message'; + + const contentDiv = document.createElement('div'); + contentDiv.className = `message-${type}`; + contentDiv.innerHTML = text.replace(/\n/g, '<br>'); + + messageDiv.appendChild(contentDiv); + this.messagesEl.appendChild(messageDiv); + + // Auto-scroll to bottom after adding message + this.scrollToBottom(); + } + + addInputMessage(code) { + const messageDiv = document.createElement('div'); + messageDiv.className = 'message'; + + const inputDiv = document.createElement('div'); + inputDiv.className = 'message-input'; + + const promptDiv = document.createElement('div'); + promptDiv.className = 'prompt'; + promptDiv.textContent = '>'; + + const codeDiv = document.createElement('div'); + codeDiv.className = 'code'; + codeDiv.textContent = code; + + inputDiv.appendChild(promptDiv); + inputDiv.appendChild(codeDiv); + messageDiv.appendChild(inputDiv); + + this.messagesEl.appendChild(messageDiv); + + // Auto-scroll to bottom after adding input message + this.scrollToBottom(); + } + + clearInput() { + this.inputEl.value = ''; + this.autoResize(); + } + + clearOutput() { + this.messagesEl.innerHTML = ''; + this.sessionCode = ''; + this.showWelcome(); + } + + autoResize() { + this.inputEl.style.height = 'auto'; + this.inputEl.style.height = Math.min(this.inputEl.scrollHeight, 120) + 'px'; + } + + scrollToBottom() { + this.messagesEl.scrollTop = this.messagesEl.scrollHeight; + } + + addToHistory(code) { + this.history.push(code); + this.historyIndex = this.history.length; + } + + navigateHistory(direction) { + if (this.history.length === 0) return; + + this.historyIndex += direction; + + if (this.historyIndex < 0) { + this.historyIndex = 0; + } else if (this.historyIndex >= this.history.length) { + this.historyIndex = this.history.length; + this.inputEl.value = ''; + } else { + this.inputEl.value = this.history[this.historyIndex]; + } + + this.autoResize(); + } + + handleSlashCommand(command) { + const cmd = command.toLowerCase(); + + switch (cmd) { + case '/help': + this.showHelp(); + break; + case '/clear': + this.clearOutput(); + break; + case '/examples': + this.loadExamples(); + break; + case '/run': + this.runSession(); + break; + case '/load': + this.fileInputEl.click(); + break; + default: + this.addMessage(`Unknown command: ${command}. Type /help for available commands.`, 'error'); + } + } + + runSession() { + if (!this.sessionCode.trim()) { + this.addMessage('No code in session to run. Load a file or type some code first.', 'info'); + return; + } + + this.addMessage('Running session code...', 'info'); + + try { + const result = this.evaluate(this.sessionCode); + + if (result.ok) { + if (result.value !== undefined) { + this.addMessage(this.formatValue(result.value), 'output'); + } + this.addMessage('Session executed successfully.', 'info'); + } else { + this.addMessage(`Error: ${result.error.message}`, 'error'); + if (result.error.codeFrame) { + this.addMessage(result.error.codeFrame, 'error'); + } + } + } catch (error) { + this.addMessage(`Unexpected error: ${error.message}`, 'error'); + } + } + + showHelp() { + const helpText = `Available slash commands: + /help - Show this help message + /clear - Clear the output + /examples - Load example code + /load - Load a .baba file + /run - Execute all code in session + +Keyboard shortcuts: + Cmd/Ctrl + Enter - Execute code + ↑/↓ - Navigate history + +Baba Yaga language features: + - Immutable variables: name : value; + - Functions: name : params -> body; + - Pattern matching: when expr is pattern then result; + - Lists: [1, 2, 3] + - Tables: {key: value} + - String concatenation: str1 .. str2 + - IO: io.out "Hello"; io.in + +Example function definition: + add : x y -> x + y; + result : add 3 4;`; + + this.addMessage(helpText, 'info'); + } + + async handleFileLoad(event) { + const file = event.target.files?.[0]; + if (!file) return; + + try { + const text = await file.text(); + + // Validate the file content by trying to parse it + const lexer = createLexer(text); + const tokens = lexer.allTokens(); + const parser = createParser(tokens); + parser.parse(); + + // If parsing succeeds, add to session and display + this.sessionCode += text + '\n'; + + this.addMessage(`Loaded ${file.name}`, 'info'); + this.addInputMessage(text); + this.addMessage('File loaded successfully. Click "Run" to execute or add more code.', 'info'); + + } catch (error) { + const message = error?.message || String(error); + this.addMessage(`Failed to load ${file.name}: ${message}`, 'error'); + } finally { + // Reset the input so the same file can be selected again + event.target.value = ''; + } + } + + loadExamples() { + const examples = [ + { + title: 'Basic arithmetic', + code: `result : 2 + 3 * 4; +io.out result;` + }, + { + title: 'Simple function', + code: `add : x y -> x + y; +result : add 5 3; +io.out result;` + }, + { + title: 'Variables and expressions', + code: `a : 10; +b : 20; +sum : a + b; +io.out sum;` + }, + { + title: 'String operations', + code: `greeting : "Hello"; +name : "World"; +message : greeting .. " " .. name; +io.out message;` + }, + { + title: 'String functions', + code: `io.out str.substring "hello world" 0 5; +io.out str.replace "hello hello" "hello" "hi";` + } + ]; + + this.addMessage('Available examples:', 'info'); + + examples.forEach(example => { + this.addMessage(`// ${example.title}`, 'output'); + this.addInputMessage(example.code); + }); + + this.addMessage('Copy and paste any example above to try it out!', 'info'); + } +} + +// Initialize the REPL when the page loads +document.addEventListener('DOMContentLoaded', () => { + new BabaYagaREPL(); +}); + diff --git a/js/baba-yaga/web/editor/README.md b/js/baba-yaga/web/editor/README.md new file mode 100644 index 0000000..3cf7c5c --- /dev/null +++ b/js/baba-yaga/web/editor/README.md @@ -0,0 +1,210 @@ +# Baba Yaga Inline AST Editor + +A web-based structural editor for the Baba Yaga programming language, featuring real-time AST visualization and **inline tree editing** capabilities. + +## **Core Concept** + +This editor allows you to edit code structure directly through the Abstract Syntax Tree (AST) view. Instead of traditional forms, you can **double-click any element** in the tree to edit it inline, making structural editing intuitive and powerful. + +## **Key Features** + +### **Inline AST Editing** +- **Double-click to Edit**: Any node type, name, value, or parameter +- **Real-time Updates**: Changes sync immediately between AST and code editor +- **Action Buttons**: + (add child) and × (delete) for each node +- **Smart Scaffolding**: Add Function, When, or With expressions with one click + +### **Layout & UX** +- **Side-by-side Layout**: Code editor (50%) + AST tree editor (50%) +- **Output Panel**: Below both panels for execution results +- **Mobile Responsive**: Stacks vertically on smaller screens +- **Auto-parsing**: Code updates in real-time as you edit + +### **Bidirectional Synchronization** +- **AST ↔ Code Editor**: Always in sync +- **Real-time Parsing**: 500ms debounced parsing as you type +- **Visual Feedback**: Parse status indicators and error highlighting + +## **Architecture** + +The editor consists of several key components: + +- **Code Editor**: Text-based input for Baba Yaga code +- **AST Tree Editor**: Interactive tree view with inline editing +- **AST Synchronizer**: Manages bidirectional synchronization +- **Baba Yaga Parser**: Real language parser integration +- **Code Generator**: Converts modified AST back to code + +## **How to Use** + +### **Basic Workflow** +1. **Type Code** → See AST tree automatically generated +2. **Double-click** any node element to edit inline +3. **Click +** to add child nodes +4. **Click ×** to delete nodes +5. **Use + Function/When/With** buttons for quick scaffolding + +### **Inline Editing** +- **Node Types**: Double-click the type (e.g., "FunctionDeclaration") +- **Names & Values**: Double-click any name or value field +- **Parameters**: Double-click parameter names or types +- **Keyboard**: Enter to save, Escape to cancel + +### **Quick Add Elements** +- **+ Function**: Creates `newFunction : -> expression` +- **+ When**: Creates empty when expression structure +- **+ With**: Creates empty with header structure + +## **Development** + +### **Setup** +1. Ensure you have a web server running (e.g., `python3 -m http.server 8000`) +2. Open `index.html` in your browser +3. The editor will automatically load and parse any existing code + +### **Key Components** +- `editor.js`: Main editor class with inline editing logic +- `ast-synchronizer.js`: AST synchronization and code generation +- `main.js`: Application initialization and global utilities + +### **Adding New Features** +To extend the inline editing capabilities: + +1. **New Node Types**: Add to `createDefaultChildNode()` method +2. **Custom Editors**: Extend the inline editing methods +3. **Validation**: Add input validation in `finishEdit*` methods +4. **Code Generation**: Update the code generation logic + +## **Future Enhancements** + +- **Syntax Highlighting**: ✅ Baba Yaga language support (implemented!) +- **Advanced Pattern Matching**: Enhanced when expression editing +- **Type System Integration**: Better type inference and validation +- **Code Suggestions**: Intelligent autocomplete and suggestions +- **Refactoring Tools**: Automated code restructuring +- **Export/Import**: Save and load AST structures +- **Undo/Redo**: Track editing history +- **Drag & Drop**: Visual AST manipulation + +## **Technical Details** + +### **Inline Editing Implementation** +- **Event Delegation**: Handles all editing through centralized methods +- **Dynamic Input Fields**: Replace text with input boxes on interaction +- **AST Manipulation**: Direct tree structure modification +- **Real-time Sync**: Immediate updates between views + +### **Performance Optimizations** +- **Debounced Parsing**: 500ms delay to avoid excessive parsing +- **Selective Updates**: Only re-render changed sections +- **Memory Management**: Efficient AST node tracking + +### **Syntax Highlighting Features** +- **Custom Language Mode**: Full Baba Yaga syntax support +- **Token Recognition**: Keywords, types, functions, operators, numbers, strings +- **Smart Indentation**: Auto-indent for function bodies, when expressions, with headers +- **Theme Integration**: Monokai dark theme with custom Baba Yaga colors +- **Code Folding**: Collapsible code blocks for better organization + +## 🌟 **Why Inline Editing?** + +Traditional structural editors require switching between different forms and panels, which can be cumbersome. This inline approach: + +- **Reduces Context Switching**: Edit directly where you see the structure +- **Improves Workflow**: Faster iteration and experimentation +- **Enhances Understanding**: Visual connection between structure and code +- **Increases Productivity**: More intuitive editing experience + +## 📁 **File Structure** + +``` +web/editor/ +├── index.html # Main HTML with inline editor layout +├── styles.css # CSS with inline editing styles +├── js/ +│ ├── editor.js # Main editor with inline editing logic +│ ├── ast-synchronizer.js # AST sync and code generation +│ ├── baba-yaga-mode.js # Custom CodeMirror language mode +│ ├── main.js # Application initialization +│ └── tree-sitter-baba-yaga.js # Future grammar integration +└── README.md # This documentation +``` + +## 🎨 **Syntax Highlighting Colors** + +The editor provides rich syntax highlighting with a carefully chosen color scheme: + +- **Keywords** (`when`, `with`, `rec`, `in`): Purple (#c586c0) +- **Types** (`Int`, `String`, `Result`): Teal (#4ec9b0) +- **Functions** (function names): Yellow (#dcdcaa) +- **Builtins** (`map`, `filter`, `reduce`): Orange (#d7ba7d) +- **Operators** (`->`, `:`, `+`, `*`): White (#d4d4d4) +- **Numbers**: Green (#b5cea8) +- **Strings**: Red (#ce9178) +- **Comments**: Green (#6a9955) +- **Variables**: Blue (#9cdcfe) + +## **Development Commands** + +The editor provides several console commands for development: + +```javascript +// Get current AST +window.babaYagaEditorCommands.getAST() + +// Get current code +window.babaYagaEditorCommands.getCode() + +// Parse current code +window.babaYagaEditorCommands.parse() + +// Format current code +window.babaYagaEditorCommands.format() + +// Show AST in console +window.babaYagaEditorCommands.showAST() + +// Show code in console +window.babaYagaEditorCommands.showCode() +``` + +## 🔧 **Baba Yaga Language Support** + +### **Supported Constructs** +- **Function Declarations**: `name : params -> body` +- **Variable Declarations**: `name : value` +- **Basic Expressions**: Arithmetic, comparison, logical operators +- **Pattern Matching**: `when` expressions (basic support) +- **Local Bindings**: `with` headers (basic support) + +### **Language Features** +- **Currying**: Multiple arrow functions +- **Type Annotations**: Optional type declarations +- **Pattern Matching**: `when` expressions with multiple cases +- **Recursion**: `with rec` for mutually recursive functions +- **Immutability**: All data structures are immutable + +## 🚀 **Getting Started** + +1. **Clone the repository** and navigate to `web/editor/` +2. **Start a web server**: `python3 -m http.server 8000` +3. **Open in browser**: Navigate to `http://localhost:8000/web/editor/` +4. **Start editing**: Type Baba Yaga code or use the inline AST editor + +## 🤝 **Contributing** + +1. Fork the repository +2. Create a feature branch +3. Make your changes +4. Test thoroughly +5. Submit a pull request + +## 📄 **License** + +This project is part of the Baba Yaga language implementation. See the main project license for details. + +## 🙏 **Acknowledgments** + +- Inspired by functional programming language editors +- Built on modern web technologies +- Designed for educational and development use diff --git a/js/baba-yaga/web/editor/debug-modules.html b/js/baba-yaga/web/editor/debug-modules.html new file mode 100644 index 0000000..549b984 --- /dev/null +++ b/js/baba-yaga/web/editor/debug-modules.html @@ -0,0 +1,107 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>Module Loading Debug</title> +</head> +<body> + <h1>Module Loading Debug</h1> + + <div id="status">Loading...</div> + + <div id="results"></div> + + <!-- Baba Yaga Language Components --> + <script src="../../lexer.js" type="module"></script> + <script src="../../parser.js" type="module"></script> + <script src="../../interpreter.js" type="module"></script> + + <script type="module"> + const statusDiv = document.getElementById('status'); + const resultsDiv = document.getElementById('results'); + + function log(message, type = 'info') { + const color = type === 'error' ? 'red' : type === 'success' ? 'green' : 'black'; + resultsDiv.innerHTML += `<p style="color: ${color}">${message}</p>`; + } + + async function debugModules() { + try { + statusDiv.textContent = 'Checking module loading...'; + + // Wait a bit for modules to load + await new Promise(resolve => setTimeout(resolve, 100)); + + log('=== Module Loading Debug ==='); + + // Check global scope + log('Global scope check:'); + log(`- window.createLexer: ${typeof window.createLexer}`); + log(`- window.createParser: ${typeof window.createParser}`); + log(`- window.createInterpreter: ${typeof window.createInterpreter}`); + + // Check if functions are available + log('Function availability check:'); + log(`- createLexer: ${typeof createLexer}`); + log(`- createParser: ${typeof createParser}`); + log(`- createInterpreter: ${typeof createInterpreter}`); + + if (typeof createLexer === 'undefined') { + log('❌ createLexer is not defined', 'error'); + } else { + log('✅ createLexer is available', 'success'); + } + + if (typeof createParser === 'undefined') { + log('❌ createParser is not defined', 'error'); + } else { + log('✅ createParser is available', 'success'); + } + + if (typeof createInterpreter === 'undefined') { + log('❌ createInterpreter is not defined', 'error'); + } else { + log('✅ createInterpreter is available', 'success'); + } + + // Try to use them if available + if (typeof createLexer !== 'undefined' && typeof createParser !== 'undefined') { + log('Testing basic functionality...'); + + try { + const testCode = 'add : x y -> x + y;'; + const lexer = createLexer(testCode); + const tokens = lexer.allTokens(); + log(`✅ Lexer test passed: ${tokens.length} tokens`); + + const parser = createParser(tokens); + const ast = parser.parse(); + log(`✅ Parser test passed: AST type = ${ast.type}`); + + statusDiv.textContent = 'All tests passed! 🎉'; + statusDiv.style.color = 'green'; + + } catch (error) { + log(`❌ Functionality test failed: ${error.message}`, 'error'); + statusDiv.textContent = 'Tests failed! ❌'; + statusDiv.style.color = 'red'; + } + } else { + log('❌ Cannot test functionality - modules not loaded', 'error'); + statusDiv.textContent = 'Modules not loaded! ❌'; + statusDiv.style.color = 'red'; + } + + } catch (error) { + log(`❌ Debug failed: ${error.message}`, 'error'); + statusDiv.textContent = 'Debug failed! ❌'; + statusDiv.style.color = 'red'; + } + } + + // Run debug when page loads + document.addEventListener('DOMContentLoaded', debugModules); + </script> +</body> +</html> diff --git a/js/baba-yaga/web/editor/index.html b/js/baba-yaga/web/editor/index.html new file mode 100644 index 0000000..6344cee --- /dev/null +++ b/js/baba-yaga/web/editor/index.html @@ -0,0 +1,577 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover"> + <title>Baba Yaga Code Runner</title> + <link rel="stylesheet" href="../../node_modules/codemirror/lib/codemirror.css"> + <link rel="stylesheet" href="../../node_modules/codemirror/theme/monokai.css"> + <script src="../../node_modules/codemirror/lib/codemirror.js"></script> + <script src="../../node_modules/codemirror/addon/edit/closebrackets.js"></script> + <script src="../../node_modules/codemirror/addon/edit/matchbrackets.js"></script> + <script src="../../node_modules/codemirror/addon/fold/foldcode.js"></script> + <script src="../../node_modules/codemirror/addon/fold/foldgutter.js"></script> + <script src="../../node_modules/codemirror/addon/fold/brace-fold.js"></script> + <script src="../../node_modules/codemirror/addon/fold/indent-fold.js"></script> + + <!-- Baba Yaga Language Components --> + <script type="module"> + // Import Baba Yaga components and make them globally available + import { createLexer, tokenTypes } from '../../lexer.js'; + import { createParser } from '../../parser.js'; + import { createInterpreter } from '../../interpreter.js'; + + // Make them globally available + window.createLexer = createLexer; + window.createParser = createParser; + window.createInterpreter = createInterpreter; + window.tokenTypes = tokenTypes; + + console.log('Baba Yaga modules loaded and made globally available'); + </script> + + <!-- Baba Yaga Language Mode --> + <script src="js/baba-yaga-mode.js"></script> + + <!-- Baba Yaga Formatter --> + <script src="js/formatter.js"></script> + + <style> + * { + margin: 0; + padding: 0; + box-sizing: border-box; + } + + html, body { + height: 100%; + overflow: hidden; + } + + body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', sans-serif; + background-color: #1e1e1e; + color: #d4d4d4; + } + + .container { + display: flex; + flex-direction: column; + height: 100vh; + overflow: hidden; + } + + .header { + background-color: #2d2d30; + border-bottom: 1px solid #3e3e42; + padding: 1rem; + display: flex; + justify-content: space-between; + align-items: center; + } + + .header h1 { + color: #569cd6; + font-size: 1.5rem; + font-weight: 600; + } + + .header-controls { + display: flex; + gap: 0.5rem; + } + + .btn { + background-color: #007acc; + color: white; + border: none; + padding: 0.5rem 1rem; + border-radius: 4px; + cursor: pointer; + font-size: 0.9rem; + transition: background-color 0.2s; + } + + .btn:hover { + background-color: #005a9e; + } + + .btn.success { + background-color: #4ec9b0; + } + + .btn.error { + background-color: #f14c4c; + } + + .main-content { + display: flex; + flex: 1; + overflow: hidden; + } + + .editor-section { + flex: 1; + display: flex; + flex-direction: column; + border-right: 1px solid #3e3e42; + } + + .editor-header { + padding: 1rem; + background-color: #2d2d30; + border-bottom: 1px solid #3e3e42; + color: #d4d4d4; + } + + .editor-container { + flex: 1; + overflow: hidden; + display: flex; + flex-direction: column; + min-height: 0; + } + + .CodeMirror { + height: 100% !important; + min-height: 300px; + flex: 1; + } + + .CodeMirror-scroll { + min-height: 100%; + } + + .output-section { + width: 400px; + display: flex; + flex-direction: column; + background-color: #252526; + } + + .output-header { + padding: 1rem; + background-color: #2d2d30; + border-bottom: 1px solid #3e3e42; + color: #d4d4d4; + } + + .output-content { + flex: 1; + overflow: auto; + padding: 1rem; + } + + .output-tabs { + display: flex; + background-color: #2d2d30; + border-bottom: 1px solid #3e3e42; + } + + .tab-btn { + background-color: transparent; + color: #d4d4d4; + border: none; + padding: 0.75rem 1rem; + cursor: pointer; + border-bottom: 2px solid transparent; + transition: all 0.2s; + } + + .tab-btn:hover { + background-color: #3e3e42; + } + + .tab-btn.active { + background-color: #007acc; + color: white; + border-bottom-color: #007acc; + } + + .tab-pane { + display: none; + } + + .tab-pane.active { + display: block; + } + + .output-text { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif; + font-size: 14px; + line-height: 1.5; + white-space: normal; + word-wrap: break-word; + } + + .error-message { + color: #f14c4c; + background-color: rgba(241, 76, 76, 0.1); + border-left: 3px solid #f14c4c; + padding: 0.75rem; + margin: 0.5rem 0; + border-radius: 4px; + } + + .success-message { + color: #4ec9b0; + background-color: rgba(78, 201, 176, 0.1); + border-left: 3px solid #4ec9b0; + border-radius: 4px; + padding: 0.75rem; + margin: 0.5rem 0; + } + + .info-message { + color: #569cd6; + background-color: rgba(86, 156, 214, 0.1); + border-left: 3px solid #569cd6; + padding: 0.75rem; + margin: 0.5rem 0; + border-radius: 4px; + } + + .sample-code-btn { + background-color: #6a9955; + margin-left: 0.5rem; + } + + .sample-code-btn:hover { + background-color: #5a8a45; + } + + .format-btn { + background-color: #ff9500; + } + + .format-btn:hover { + background-color: #e6850e; + } + + .structural-editor-btn { + background-color: #8b5cf6; + } + + .structural-editor-btn:hover { + background-color: #7c3aed; + } + + /* Custom token colors for Baba Yaga - override monokai theme */ + .cm-s-monokai .cm-keyword { + color: #c586c0 !important; + font-weight: bold; + } + + .cm-s-monokai .cm-type { + color: #4ec9b0 !important; + font-weight: bold; + } + + .cm-s-monokai .cm-function { + color: #dcdcaa !important; + font-weight: bold; + } + + .cm-s-monokai .cm-builtin { + color: #d7ba7d !important; + } + + .cm-s-monokai .cm-operator { + color: #d4d4d4 !important; + } + + .cm-s-monokai .cm-number { + color: #b5cea8 !important; + } + + .cm-s-monokai .cm-string { + color: #ce9178 !important; + } + + .cm-s-monokai .cm-comment { + color: #6a9955 !important; + font-style: italic; + } + + .cm-s-monokai .cm-variable { + color: #9cdcfe !important; + } + + /* Dark theme adjustments for monokai */ + .cm-s-monokai.CodeMirror { + background-color: #1e1e1e; + color: #d4d4d4; + } + + .cm-s-monokai .CodeMirror-gutters { + background-color: #2d2d30; + border-right: 1px solid #3e3e42; + } + + .cm-s-monokai .CodeMirror-linenumber { + color: #858585; + } + + .CodeMirror-focused .CodeMirror-cursor { + border-left: 2px solid #007acc; + } + + .CodeMirror-selected { + background-color: #264f78 !important; + } + + .CodeMirror-activeline-background { + background-color: #2d2d30; + } + + /* Touch-friendly improvements */ + .btn { + touch-action: manipulation; + -webkit-tap-highlight-color: transparent; + } + + .tab-btn { + touch-action: manipulation; + -webkit-tap-highlight-color: transparent; + } + + /* Ensure CodeMirror is mobile-friendly */ + .CodeMirror { + touch-action: manipulation; + -webkit-overflow-scrolling: touch; + } + + /* Mobile scrollbar improvements */ + .output-content::-webkit-scrollbar { + width: 6px; + } + + .output-content::-webkit-scrollbar-track { + background: #2d2d30; + } + + .output-content::-webkit-scrollbar-thumb { + background: #3e3e42; + border-radius: 3px; + } + + .output-content::-webkit-scrollbar-thumb:hover { + background: #4e4e52; + } + + /* Improved output layout */ + .output-content { + padding: 1rem; + } + + .output-content .success-message, + .output-content .info-message, + .output-content .error-message { + margin: 0.75rem 0; + } + + .output-content .info-message:first-child { + margin-top: 0; + } + + /* IO output items */ + .io-output-item { + margin: 0.25rem 0; + padding: 0.5rem; + background: rgba(86, 156, 214, 0.1); + border-radius: 4px; + border-left: 2px solid #569cd6; + font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; + font-size: 13px; + } + + /* Mobile Responsive Design */ + @media (max-width: 768px) { + .header { + flex-direction: column; + gap: 1rem; + padding: 1rem 0.5rem; + } + + .header h1 { + font-size: 1.25rem; + text-align: center; + } + + .header-controls { + justify-content: center; + flex-wrap: wrap; + gap: 0.5rem; + } + + .btn { + padding: 0.5rem 0.75rem; + font-size: 0.85rem; + min-width: 80px; + } + + .main-content { + flex-direction: column; + } + + .editor-section { + border-right: none; + border-bottom: 1px solid #3e3e42; + min-height: 300px; + } + + .output-section { + width: 100%; + min-height: 250px; + } + + .editor-header, + .output-header { + padding: 0.75rem; + } + + .editor-header h3, + .output-header h3 { + font-size: 1rem; + } + + .output-tabs { + flex-wrap: wrap; + } + + .tab-btn { + padding: 0.5rem 0.75rem; + font-size: 0.85rem; + flex: 1; + min-width: 80px; + text-align: center; + } + + .output-content { + padding: 0.75rem; + } + + .success-message, + .info-message, + .error-message { + padding: 0.5rem; + margin: 0.5rem 0; + font-size: 0.9rem; + } + + .io-output-item { + padding: 0.4rem; + font-size: 12px; + } + + .CodeMirror { + min-height: 250px; + } + } + + /* Small mobile devices */ + @media (max-width: 480px) { + .header h1 { + font-size: 1.1rem; + } + + .btn { + padding: 0.4rem 0.6rem; + font-size: 0.8rem; + min-width: 70px; + } + + .editor-header, + .output-header { + padding: 0.5rem; + } + + .output-content { + padding: 0.5rem; + } + + .success-message, + .info-message, + .error-message { + padding: 0.4rem; + font-size: 0.85rem; + } + + .io-output-item { + padding: 0.3rem; + font-size: 11px; + } + } + + /* Landscape mobile optimization */ + @media (max-width: 768px) and (orientation: landscape) { + .main-content { + flex-direction: row; + } + + .editor-section { + border-right: 1px solid #3e3e42; + border-bottom: none; + min-height: 200px; + } + + .output-section { + width: 300px; + min-height: 200px; + } + } + </style> +</head> +<body> + <div class="container"> + <!-- Header --> + <header class="header"> + <h1>Baba Yaga Code Runner</h1> + <div class="header-controls"> + <button id="run-btn" class="btn">▶ Run Code</button> + <button id="format-btn" class="btn format-btn">📝 Format</button> + <button id="sample-btn" class="btn sample-code-btn">Load Sample</button> + </div> + </header> + + <!-- Main content --> + <div class="main-content"> + <!-- Code Editor Section --> + <div class="editor-section"> + <div class="editor-header"> + <h3>Code Editor</h3> + </div> + <div class="editor-container"> + <textarea id="code-editor" placeholder="Enter your Baba Yaga code here..."></textarea> + </div> + </div> + + <!-- Output Section --> + <div class="output-section"> + <div class="output-header"> + <h3>Output</h3> + </div> + <div class="output-tabs"> + <button class="tab-btn active" data-tab="output">Output</button> + <button class="tab-btn" data-tab="errors">Errors</button> + <button class="tab-btn" data-tab="ast">AST</button> + </div> + <div class="output-content"> + <div id="output-tab" class="tab-pane active"> + <div class="output-text" id="output-text">Ready to run Baba Yaga code...</div> + </div> + <div id="errors-tab" class="tab-pane"> + <div class="output-text" id="errors-text">No errors yet.</div> + </div> + <div id="ast-tab" class="tab-pane"> + <div class="output-text" id="ast-text">AST will appear here after parsing.</div> + </div> + </div> + </div> + </div> + </div> + + <!-- Scripts --> + <script src="js/baba-yaga-runner.js"></script> +</body> +</html> diff --git a/js/baba-yaga/web/editor/js/ast-synchronizer.js b/js/baba-yaga/web/editor/js/ast-synchronizer.js new file mode 100644 index 0000000..f404c0c --- /dev/null +++ b/js/baba-yaga/web/editor/js/ast-synchronizer.js @@ -0,0 +1,463 @@ +/** + * ASTSynchronizer - Manages bidirectional synchronization between text and structural views + * This is the core component that keeps the AST, text editor, and structural editors in sync + */ +class ASTSynchronizer { + constructor(editor, structuralEditors) { + this.editor = editor; + this.structuralEditors = structuralEditors; + this.ast = null; + this.isUpdating = false; // Prevent infinite loops during updates + + this.init(); + } + + init() { + // Set up change listeners + this.setupChangeListeners(); + } + + setupChangeListeners() { + // Listen for AST changes from the editor + if (this.editor && this.editor.editor) { + this.editor.editor.on('change', () => { + if (!this.isUpdating) { + this.updateAST(); + } + }); + } + + // Listen for structural editor changes + if (this.structuralEditors) { + this.structuralEditors.onChange((changes) => { + if (!this.isUpdating) { + this.updateASTFromStructural(changes); + } + }); + } + } + + updateAST() { + try { + this.isUpdating = true; + + // Show parsing status + this.showParseStatus('parsing'); + + const code = this.editor.getCode(); + if (!code.trim()) { + this.ast = { type: 'Program', body: [] }; + this.showParseStatus('parsed'); + return; + } + + // Parse the code to get new AST + const newAST = this.parseCode(code); + this.ast = newAST; + + // Update structural editors with new AST (but don't trigger changes) + if (this.structuralEditors) { + this.structuralEditors.updateFromAST(newAST, true); // true = silent update + } + + // Update tree view + this.updateTreeView(newAST); + + // Show success status + this.showParseStatus('parsed'); + + } catch (error) { + console.error('Error updating AST:', error); + this.showParseError(error); + this.showParseStatus('error'); + } finally { + this.isUpdating = false; + } + } + + updateASTFromStructural(changes) { + try { + this.isUpdating = true; + + // Apply changes to the current AST + const updatedAST = this.applyStructuralChanges(this.ast, changes); + this.ast = updatedAST; + + // Generate code from updated AST + const newCode = this.generateCode(updatedAST); + + // Only update text editor if we have actual structural changes + if (changes && changes.length > 0) { + // Update text editor + this.editor.editor.setValue(newCode); + } + + // Update tree view + this.updateTreeView(updatedAST); + + } catch (error) { + console.error('Error updating AST from structural changes:', error); + this.showStructuralError(error); + } finally { + this.isUpdating = false; + } + } + + parseCode(code) { + // Use the real Baba Yaga parser + if (this.editor.parseWithBabaYaga) { + return this.editor.parseWithBabaYaga(code); + } + + // Fallback parsing + return this.fallbackParse(code); + } + + fallbackParse(code) { + const lines = code.split('\n').filter(line => line.trim()); + const ast = { + type: 'Program', + body: [] + }; + + lines.forEach((line, index) => { + const trimmed = line.trim(); + if (trimmed && !trimmed.startsWith('//')) { + if (trimmed.includes(':')) { + const [name, ...rest] = trimmed.split(':'); + const value = rest.join(':').trim(); + + if (value.includes('->')) { + // Function declaration + ast.body.push({ + type: 'FunctionDeclaration', + name: name.trim(), + params: this.parseFunctionParams(value), + body: this.parseFunctionBody(value), + returnType: null, + line: index + 1 + }); + } else { + // Variable declaration + ast.body.push({ + type: 'VariableDeclaration', + name: name.trim(), + value: value, + line: index + 1 + }); + } + } + } + }); + + return ast; + } + + parseFunctionParams(value) { + const arrowIndex = value.indexOf('->'); + if (arrowIndex === -1) return []; + + const beforeArrow = value.substring(0, arrowIndex).trim(); + if (!beforeArrow) return []; + + // Check if this is a typed function signature + if (beforeArrow.startsWith('(') && beforeArrow.endsWith(')')) { + return this.parseTypedParameters(beforeArrow); + } + + // Simple space-separated parameter parsing + return beforeArrow.split(/\s+/).filter(p => p.trim()); + } + + parseTypedParameters(paramString) { + // Parse (x: Int, y: String) format + const content = paramString.slice(1, -1); // Remove parentheses + if (!content.trim()) return []; + + const params = []; + const parts = content.split(',').map(p => p.trim()); + + parts.forEach(part => { + if (part.includes(':')) { + const [name, type] = part.split(':').map(p => p.trim()); + params.push({ name, type }); + } else { + params.push({ name: part, type: null }); + } + }); + + return params; + } + + parseFunctionBody(value) { + const arrowIndex = value.indexOf('->'); + if (arrowIndex === -1) return ''; + + const afterArrow = value.substring(arrowIndex + 2).trim(); + + // Check if this is a when expression + if (afterArrow.startsWith('when')) { + return this.parseWhenExpression(afterArrow); + } + + // Check if this is a with header + if (afterArrow.startsWith('with')) { + return this.parseWithHeader(afterArrow); + } + + return afterArrow; + } + + parseWhenExpression(whenCode) { + // Basic when expression parsing + return { + type: 'WhenExpression', + raw: whenCode + }; + } + + parseWithHeader(withCode) { + // Basic with header parsing + return { + type: 'WithHeader', + raw: withCode + }; + } + + applyStructuralChanges(ast, changes) { + if (!ast) return ast; + + const updatedAST = JSON.parse(JSON.stringify(ast)); // Deep clone + + changes.forEach(change => { + switch (change.type) { + case 'function_update': + this.updateFunctionInAST(updatedAST, change); + break; + case 'when_update': + this.updateWhenInAST(updatedAST, change); + break; + case 'with_update': + this.updateWithInAST(updatedAST, change); + break; + case 'add_function': + this.addFunctionToAST(updatedAST, change); + break; + case 'remove_function': + this.removeFunctionFromAST(updatedAST, change); + break; + default: + console.warn('Unknown change type:', change.type); + } + }); + + return updatedAST; + } + + updateFunctionInAST(ast, change) { + const functionIndex = ast.body.findIndex(node => + node.type === 'FunctionDeclaration' && node.name === change.oldName + ); + + if (functionIndex !== -1) { + ast.body[functionIndex] = { + type: 'FunctionDeclaration', + name: change.newName || change.oldName, + params: change.params || ast.body[functionIndex].params, + body: change.body || ast.body[functionIndex].body, + returnType: change.returnType || ast.body[functionIndex].returnType, + line: ast.body[functionIndex].line + }; + } + } + + updateWhenInAST(ast, change) { + // Find and update when expressions in function bodies + this.updateWhenInNode(ast, change); + } + + updateWhenInNode(node, change) { + if (node.type === 'WhenExpression') { + // Update the when expression + Object.assign(node, change); + } else if (node.body && typeof node.body === 'object') { + this.updateWhenInNode(node.body, change); + } else if (Array.isArray(node.body)) { + node.body.forEach(child => this.updateWhenInNode(child, change)); + } + } + + updateWithInAST(ast, change) { + // Find and update with headers in function bodies + this.updateWithInNode(ast, change); + } + + updateWithInNode(node, change) { + if (node.type === 'WithHeader') { + // Update the with header + Object.assign(node, change); + } else if (node.body && typeof node.body === 'object') { + this.updateWithInNode(node.body, change); + } else if (Array.isArray(node.body)) { + node.body.forEach(child => this.updateWithInNode(child, change)); + } + } + + addFunctionToAST(ast, change) { + const newFunction = { + type: 'FunctionDeclaration', + name: change.name, + params: change.params || [], + body: change.body || '', + returnType: change.returnType || null, + line: ast.body.length + 1 + }; + + ast.body.push(newFunction); + } + + removeFunctionFromAST(ast, change) { + const functionIndex = ast.body.findIndex(node => + node.type === 'FunctionDeclaration' && node.name === change.name + ); + + if (functionIndex !== -1) { + ast.body.splice(functionIndex, 1); + } + } + + generateCode(ast) { + if (!ast || ast.type !== 'Program') return ''; + + const lines = []; + + ast.body.forEach(node => { + switch (node.type) { + case 'FunctionDeclaration': + lines.push(this.generateFunctionCode(node)); + break; + case 'VariableDeclaration': + lines.push(this.generateVariableCode(node)); + break; + default: + lines.push(`// Unknown node type: ${node.type}`); + } + }); + + return lines.join('\n'); + } + + generateFunctionCode(node) { + let code = `${node.name} : `; + + // Generate parameter list + if (node.params && node.params.length > 0) { + if (typeof node.params[0] === 'string') { + // Untyped parameters + code += node.params.join(' '); + } else { + // Typed parameters + const paramStrings = node.params.map(param => + param.type ? `${param.name}: ${param.type}` : param.name + ); + code += `(${paramStrings.join(', ')})`; + } + } + + // Add return type if specified + if (node.returnType) { + code += ` -> ${node.returnType}`; + } + + code += ' -> '; + + // Generate body + if (node.body && typeof node.body === 'object') { + if (node.body.type === 'WhenExpression') { + code += this.generateWhenCode(node.body); + } else if (node.body.type === 'WithHeader') { + code += this.generateWithCode(node.body); + } else { + code += node.body.raw || JSON.stringify(node.body); + } + } else { + code += node.body || ''; + } + + return code; + } + + generateVariableCode(node) { + return `${node.name} : ${node.value};`; + } + + generateWhenCode(whenNode) { + // Basic when expression code generation + return whenNode.raw || 'when expression'; + } + + generateWithCode(withNode) { + // Basic with header code generation + return withNode.raw || 'with header'; + } + + updateTreeView(ast) { + if (this.editor.updateTreeView && ast) { + try { + this.editor.updateTreeView(ast); + } catch (error) { + console.error('Error updating tree view:', error); + } + } + } + + showParseError(error) { + if (this.editor.showError) { + this.editor.showError('Parse error: ' + error.message); + } + } + + showStructuralError(error) { + if (this.editor.showError) { + this.editor.showError('Structural edit error: ' + error.message); + } + } + + getAST() { + return this.ast; + } + + setAST(ast) { + this.ast = ast; + this.updateTreeView(ast); + } + + showParseStatus(status) { + const statusElement = document.getElementById('parse-status'); + if (statusElement) { + statusElement.className = `parse-status ${status}`; + + switch (status) { + case 'parsing': + statusElement.textContent = '⏳ Parsing...'; + break; + case 'parsed': + statusElement.textContent = '✅ Parsed'; + // Clear status after 2 seconds + setTimeout(() => { + statusElement.textContent = ''; + statusElement.className = 'parse-status'; + }, 2000); + break; + case 'error': + statusElement.textContent = '❌ Parse Error'; + // Clear status after 3 seconds + setTimeout(() => { + statusElement.textContent = ''; + statusElement.className = 'parse-status'; + }, 3000); + break; + } + } + } +} diff --git a/js/baba-yaga/web/editor/js/baba-yaga-mode.js b/js/baba-yaga/web/editor/js/baba-yaga-mode.js new file mode 100644 index 0000000..32b5421 --- /dev/null +++ b/js/baba-yaga/web/editor/js/baba-yaga-mode.js @@ -0,0 +1,191 @@ +/** + * Baba Yaga Language Mode for CodeMirror + * Provides syntax highlighting for the Baba Yaga functional programming language + */ + +// Global function to initialize the Baba Yaga language mode +window.initBabaYagaMode = function() { + // Check if CodeMirror is available + if (typeof CodeMirror === 'undefined') { + console.log('CodeMirror not available yet, will retry...'); + setTimeout(window.initBabaYagaMode, 100); + return; + } + + console.log('Initializing Baba Yaga language mode...'); + + // Baba Yaga language keywords + const keywords = [ + 'when', 'is', 'then', 'else', 'with', 'rec', 'in', + 'Ok', 'Err', 'true', 'false', 'PI', 'INFINITY', + 'and', 'or', 'xor', 'not', 'if' + ]; + + // Baba Yaga type system + const types = [ + 'Int', 'String', 'Result', 'Float', 'Number', + 'List', 'Table', 'Bool', 'Unit', 'Maybe' + ]; + + // Function names that are commonly used + const builtins = [ + 'map', 'filter', 'reduce', 'fold', 'head', 'tail', + 'length', 'append', 'concat', 'reverse', 'sort' + ]; + + // Main tokenizer function + function tokenize(stream, state) { + // Handle comments + if (stream.match(/\/\/.*/)) { + return 'comment'; + } + + // Handle multi-line comments + if (stream.match(/\/\*/)) { + state.inComment = true; + return 'comment'; + } + + if (state.inComment) { + if (stream.match(/\*\//)) { + state.inComment = false; + } else { + stream.next(); + } + return 'comment'; + } + + // Handle whitespace + if (stream.eatSpace()) { + return null; + } + + // Handle numbers (integers and floats) + if (stream.match(/^-?\d+\.\d+/)) { + return 'number'; + } + if (stream.match(/^-?\d+/)) { + return 'number'; + } + + // Handle strings + if (stream.match(/^"[^"]*"/)) { + return 'string'; + } + + // Handle identifiers and keywords + if (stream.match(/^[a-zA-Z_][a-zA-Z0-9_]*/)) { + const word = stream.current(); + + if (keywords.includes(word)) { + return 'keyword'; + } + + if (types.includes(word)) { + return 'type'; + } + + if (builtins.includes(word)) { + return 'builtin'; + } + + // Check if it's a function declaration (followed by :) + const nextChar = stream.peek(); + if (nextChar === ':') { + return 'function'; + } + + return 'variable'; + } + + // Handle operators and symbols + if (stream.match(/^->/)) { + return 'operator'; + } + + if (stream.match(/^[=!<>]=/)) { + return 'operator'; + } + + if (stream.match(/^[+\-*/%=<>!&|^,;:()[\]{}]/)) { + return 'operator'; + } + + // Handle dots for member access + if (stream.match(/^\./)) { + return 'operator'; + } + + // Handle unknown characters + stream.next(); + return null; + } + + // Define the Baba Yaga language mode + CodeMirror.defineMode("baba-yaga", function() { + return { + startState: function() { + return { + inComment: false, + inString: false, + indentLevel: 0 + }; + }, + + token: function(stream, state) { + return tokenize(stream, state); + }, + + // Indentation rules + indent: function(state, textAfter) { + const baseIndent = state.indentLevel * 2; + + // Increase indent after certain patterns + if (textAfter.match(/^[a-zA-Z_][a-zA-Z0-9_]*\s*:/)) { + return baseIndent + 2; + } + + if (textAfter.match(/^->/)) { + return baseIndent + 2; + } + + if (textAfter.match(/^when/)) { + return baseIndent + 2; + } + + if (textAfter.match(/^with/)) { + return baseIndent + 2; + } + + return baseIndent; + }, + + // Line comment character + lineComment: "//", + + // Auto-indent on certain characters + electricChars: "{}:->", + + // Fold code blocks + fold: "indent" + }; + }); + + // Note: CodeMirror 5 doesn't have defineTheme, we use CSS instead + // The theme is defined in the CSS with .cm-s-baba-yaga classes + + // Also register MIME types + CodeMirror.defineMIME("text/x-baba-yaga", "baba-yaga"); + CodeMirror.defineMIME("text/baba-yaga", "baba-yaga"); + CodeMirror.defineMIME("application/x-baba-yaga", "baba-yaga"); + + console.log('Baba Yaga language mode loaded successfully!'); + console.log('Available modes:', Object.keys(CodeMirror.modes)); + + // Dispatch a custom event to notify that the mode is ready + window.dispatchEvent(new CustomEvent('baba-yaga-mode-ready')); +}; + +// Start initialization +console.log('Baba Yaga mode script loaded, waiting for CodeMirror...'); +window.initBabaYagaMode(); diff --git a/js/baba-yaga/web/editor/js/baba-yaga-runner.js b/js/baba-yaga/web/editor/js/baba-yaga-runner.js new file mode 100644 index 0000000..6dd0312 --- /dev/null +++ b/js/baba-yaga/web/editor/js/baba-yaga-runner.js @@ -0,0 +1,564 @@ +class BabaYagaRunner { + constructor() { + this.editor = null; + this.container = document.querySelector('.container'); + this.currentIOOutput = []; + this.init(); + } + + async init() { + try { + // Wait for Baba Yaga language mode to be ready + await this.waitForLanguageMode(); + + // Initialize CodeMirror editor + this.initEditor(); + + // Set up event listeners + this.setupEventListeners(); + + // Load sample code + this.loadSampleCode(); + + console.log('Baba Yaga Code Runner initialized successfully'); + } catch (error) { + console.error('Failed to initialize Baba Yaga Code Runner:', error); + this.showError('Initialization failed: ' + error.message); + } + } + + async waitForLanguageMode() { + return new Promise((resolve) => { + if (window.CodeMirror && window.CodeMirror.modes['baba-yaga']) { + resolve(); + } else { + window.addEventListener('baba-yaga-mode-ready', resolve); + } + }); + } + + initEditor() { + const textarea = document.getElementById('code-editor'); + + if (typeof CodeMirror === 'undefined') { + console.warn('CodeMirror not available, using basic textarea'); + return; + } + + // Check if Baba Yaga language mode found, initializing with syntax highlighting + if (CodeMirror.modes['baba-yaga']) { + console.log('Baba Yaga language mode found, initializing with syntax highlighting'); + this.editor = CodeMirror.fromTextArea(textarea, { + mode: 'baba-yaga', + theme: 'monokai', + lineNumbers: true, + autoCloseBrackets: true, + matchBrackets: true, + indentUnit: 2, + tabSize: 2, + indentWithTabs: false, + lineWrapping: true, + foldGutter: true, + gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'], + // Mobile-friendly settings + lineWiseCopyCut: false, + dragDrop: false, + extraKeys: { + 'Tab': 'indentMore', + 'Shift-Tab': 'indentLess', + 'Enter': 'newlineAndIndent', + 'Ctrl-Enter': () => this.runCode(), + 'Cmd-Enter': () => this.runCode() + } + }); + + // Ensure CodeMirror fills the container properly + setTimeout(() => { + this.editor.refresh(); + }, 100); + } else { + console.warn('Baba Yaga language mode not found, using plain text mode'); + this.editor = CodeMirror.fromTextArea(textarea, { + mode: 'text', + theme: 'monokai', + lineNumbers: true, + autoCloseBrackets: true, + matchBrackets: true, + indentUnit: 2, + tabSize: 2, + indentWithTabs: false, + lineWrapping: true, + foldGutter: true, + gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'], + // Mobile-friendly settings + lineWiseCopyCut: false, + dragDrop: false, + extraKeys: { + 'Tab': 'indentMore', + 'Shift-Tab': 'indentLess', + 'Enter': 'newlineAndIndent', + 'Ctrl-Enter': () => this.runCode(), + 'Cmd-Enter': () => this.runCode() + } + }); + } + } + + setupEventListeners() { + // Run button + const runBtn = document.getElementById('run-btn'); + if (runBtn) { + runBtn.addEventListener('click', () => this.runCode()); + // Add touch event for mobile + runBtn.addEventListener('touchend', (e) => { + e.preventDefault(); + this.runCode(); + }); + } + + // Format button + const formatBtn = document.getElementById('format-btn'); + if (formatBtn) { + formatBtn.addEventListener('click', () => this.formatCode()); + // Add touch event for mobile + formatBtn.addEventListener('touchend', (e) => { + e.preventDefault(); + this.formatCode(); + }); + } + + // Sample code button + const sampleBtn = document.getElementById('sample-btn'); + if (sampleBtn) { + sampleBtn.addEventListener('click', () => this.loadSampleCode()); + // Add touch event for mobile + sampleBtn.addEventListener('touchend', (e) => { + e.preventDefault(); + this.loadSampleCode(); + }); + } + + // Tab switching + const tabBtns = document.querySelectorAll('.output-tabs .tab-btn'); + tabBtns.forEach(btn => { + btn.addEventListener('click', () => this.switchTab(btn.dataset.tab)); + // Add touch event for mobile + btn.addEventListener('touchend', (e) => { + e.preventDefault(); + this.switchTab(btn.dataset.tab); + }); + }); + + // Handle mobile keyboard events + this.setupMobileKeyboardHandling(); + } + + setupMobileKeyboardHandling() { + // Handle mobile virtual keyboard events + if (this.editor) { + // Ensure CodeMirror handles mobile input properly + this.editor.on('focus', () => { + // Scroll into view on mobile when focusing + if (window.innerWidth <= 768) { + setTimeout(() => { + this.editor.refresh(); + }, 100); + } + }); + + // Handle mobile viewport changes + window.addEventListener('resize', () => { + if (this.editor && this.editor.refresh) { + setTimeout(() => { + this.editor.refresh(); + }, 100); + } + }); + } + } + + switchTab(tabName) { + // Hide all tab panes + const tabPanes = document.querySelectorAll('.tab-pane'); + tabPanes.forEach(pane => pane.classList.remove('active')); + + // Remove active class from all tab buttons + const tabBtns = document.querySelectorAll('.output-tabs .tab-btn'); + tabBtns.forEach(btn => btn.classList.remove('active')); + + // Show selected tab pane + const selectedPane = document.getElementById(`${tabName}-tab`); + if (selectedPane) { + selectedPane.classList.add('active'); + } + + // Activate selected tab button + const selectedBtn = document.querySelector(`[data-tab="${tabName}"]`); + if (selectedBtn) { + selectedBtn.classList.add('active'); + } + } + + async runCode() { + const runBtn = document.getElementById('run-btn'); + const originalText = runBtn.textContent; + + try { + // Update button state + runBtn.textContent = 'Running...'; + runBtn.className = 'btn'; + runBtn.disabled = true; + + // Clear previous output + this.clearOutput(); + + // Clear IO output tracking + this.currentIOOutput = []; + + // Get code from editor + const code = this.getCode(); + if (!code.trim()) { + this.showError('No code to run. Please enter some Baba Yaga code.'); + return; + } + + // Parse and execute + const result = await this.executeCode(code); + + // Display results + this.displayResults(result); + + // Update button state + runBtn.textContent = 'Success!'; + runBtn.className = 'btn success'; + + } catch (error) { + console.error('Code execution error:', error); + this.showError('Execution failed: ' + error.message); + + // Update button state + runBtn.textContent = 'Error'; + runBtn.className = 'btn error'; + } finally { + // Reset button after delay + setTimeout(() => { + runBtn.textContent = originalText; + runBtn.className = 'btn'; + runBtn.disabled = false; + }, 2000); + } + } + + async executeCode(code) { + const result = { + code: code, + ast: null, + output: null, + errors: [], + executionTime: 0, + ioOutput: [] // Track IO output + }; + + try { + // Step 1: Lexical Analysis + this.showInfo('Analyzing code...'); + const lexer = createLexer(code); + const tokens = lexer.allTokens(); + this.showInfo(`Generated ${tokens.length} tokens`); + + // Step 2: Parsing + this.showInfo('Building AST...'); + const parser = createParser(tokens); + const ast = parser.parse(); + result.ast = ast; + this.showInfo('AST built successfully'); + + // Step 3: Execution + this.showInfo('Executing code...'); + const startTime = performance.now(); + console.log('Creating interpreter with AST:', ast); + + // Create a custom IO host that captures output + const ioHost = { + io: { + out: (...args) => { + // Capture output and display it in real-time + const outputText = args.map(arg => + typeof arg === 'object' ? JSON.stringify(arg, null, 2) : String(arg) + ).join(' '); + + // Store in result for later display + result.ioOutput.push(outputText); + + // Also store in instance for real-time display + this.currentIOOutput.push(outputText); + + // Show real-time output in a cleaner format + this.showInfo(`${outputText}`); + console.log('Baba Yaga output:', ...args); + }, + in: () => '', // No input for now + emit: () => {}, // No events for now + listen: () => () => {} // No listeners for now + } + }; + + const interpreter = createInterpreter(ast, ioHost); + console.log('Interpreter created:', interpreter); + console.log('Interpreter methods:', Object.getOwnPropertyNames(interpreter)); + const output = interpreter.interpret(); + console.log('Execution output:', output); + result.executionTime = performance.now() - startTime; + result.output = output; + + this.showInfo(`⚡ Execution completed in ${result.executionTime.toFixed(2)}ms`); + + } catch (error) { + // Enhanced error handling with helpful messages + const errorInfo = this.enhanceErrorMessage(error, code); + result.errors.push(errorInfo); + throw new Error(errorInfo.message); + } + + return result; + } + + enhanceErrorMessage(error, code) { + let enhancedError = { + message: error.message, + line: null, + column: null, + suggestion: '', + context: '' + }; + + // Extract line and column information from error message + const lineMatch = error.message.match(/at (\d+):(\d+)/); + if (lineMatch) { + enhancedError.line = parseInt(lineMatch[1]); + enhancedError.column = parseInt(lineMatch[2]); + + // Get the problematic line + const lines = code.split('\n'); + if (enhancedError.line <= lines.length) { + const problemLine = lines[enhancedError.line - 1]; + enhancedError.context = problemLine; + + // Add helpful suggestions based on error type + if (error.message.includes('Unexpected token')) { + enhancedError.suggestion = 'Check for missing operators, parentheses, or semicolons.'; + } else if (error.message.includes('Expected')) { + enhancedError.suggestion = 'Check for missing required syntax elements.'; + } else if (error.message.includes('ARROW')) { + enhancedError.suggestion = 'Make sure arrow functions use the correct syntax: `params -> body`.'; + } else if (error.message.includes('COLON')) { + enhancedError.suggestion = 'Function declarations need a colon: `name : params -> body`.'; + } + } + } + + return enhancedError; + } + + displayResults(result) { + // Display AST + const astText = document.getElementById('ast-text'); + if (astText) { + astText.innerHTML = `<pre>${JSON.stringify(result.ast, null, 2)}</pre>`; + } + + // Display output + const outputText = document.getElementById('output-text'); + if (outputText) { + let outputHtml = ''; + + // Show execution status + if (result.output !== null && result.output !== undefined) { + outputHtml += ` + <div class="success-message"> + <strong>Execution successful!</strong><br> + <strong>Return value:</strong> ${typeof result.output === 'object' ? JSON.stringify(result.output, null, 2) : result.output}<br> + <strong>Execution time:</strong> ${result.executionTime.toFixed(2)}ms + </div> + `; + } else { + outputHtml += ` + <div class="info-message"> + <strong>Code executed successfully</strong><br> + <strong>Execution time:</strong> ${result.executionTime.toFixed(2)}ms<br> + <em>No return value (this is normal for some programs)</em> + </div> + `; + } + + // Show IO output if any + if (result.ioOutput && result.ioOutput.length > 0) { + outputHtml += ` + <div class="info-message" style="margin-top: 1rem;"> + <strong>IO Output:</strong> + ${result.ioOutput.map(output => `<div class="io-output-item">${output}</div>`).join('')} + </div> + `; + } + + outputText.innerHTML = outputHtml; + } + + // Switch to output tab + this.switchTab('output'); + } + + showError(message) { + const errorsText = document.getElementById('errors-text'); + if (errorsText) { + errorsText.innerHTML = ` + <div class="error-message"> + <strong>Error:</strong><br> + ${message} + </div> + `; + } + + // Switch to errors tab + this.switchTab('errors'); + } + + showInfo(message) { + const outputText = document.getElementById('output-text'); + if (outputText) { + outputText.innerHTML = message; + } + } + + clearOutput() { + const outputText = document.getElementById('output-text'); + const errorsText = document.getElementById('errors-text'); + const astText = document.getElementById('ast-text'); + + if (outputText) outputText.textContent = ''; + if (errorsText) errorsText.textContent = 'No errors yet.'; + if (astText) astText.textContent = 'AST will appear here after parsing.'; + } + + async formatCode() { + const formatBtn = document.getElementById('format-btn'); + const originalText = formatBtn.textContent; + + try { + // Update button state + formatBtn.textContent = 'Formatting...'; + formatBtn.className = 'btn format-btn'; + formatBtn.disabled = true; + + // Get code from editor + const code = this.getCode(); + if (!code.trim()) { + this.showError('No code to format. Please enter some Baba Yaga code.'); + return; + } + + // Check if formatter is available + if (typeof BabaYagaFormatter === 'undefined') { + this.showError('Formatter not available. Please refresh the page.'); + return; + } + + // Format the code + const formatter = new BabaYagaFormatter({ + indentSize: 2, + maxLineLength: 100 + }); + + const formattedCode = formatter.format(code); + + // Update the editor with formatted code + if (this.editor) { + this.editor.setValue(formattedCode); + } else { + document.getElementById('code-editor').value = formattedCode; + } + + // Show success message + this.showInfo('Code formatted successfully!'); + this.switchTab('output'); + + // Update button state + formatBtn.textContent = 'Formatted!'; + formatBtn.className = 'btn format-btn success'; + + } catch (error) { + console.error('Code formatting error:', error); + this.showError('Formatting failed: ' + error.message); + + // Update button state + formatBtn.textContent = 'Error'; + formatBtn.className = 'btn format-btn error'; + } finally { + // Reset button after delay + setTimeout(() => { + formatBtn.textContent = originalText; + formatBtn.className = 'btn format-btn'; + formatBtn.disabled = false; + }, 2000); + } + } + + getCode() { + if (this.editor) { + return this.editor.getValue(); + } + return document.getElementById('code-editor').value; + } + + loadSampleCode() { + const sampleCode = `// Sample Baba Yaga code - try running this! +// Notice the inconsistent formatting - use the Format button to clean it up! +add:x y->x+y; + +multiply : x y->x*y; + +// Calculate factorial with inconsistent spacing +factorial:n-> + when n is + 0 then 1 + 1 then 1 + _ then n*factorial(n-1); + +// Test the functions +result1:add 5 3; +result2:multiply 4 6; +result3:factorial 5; + +// Use io.out to display results +io.out"Results:"; +io.out"add 5 3 = "result1; +io.out "multiply 4 6 = " result2; +io.out"factorial 5 = "result3; + +// Return the factorial result +result3`; + + if (this.editor) { + this.editor.setValue(sampleCode); + } else { + document.getElementById('code-editor').value = sampleCode; + } + + this.showInfo('Sample code loaded! Click "Run Code" to execute it.'); + this.switchTab('output'); + } +} + +// Initialize the runner when the page loads +document.addEventListener('DOMContentLoaded', () => { + window.babaYagaRunner = new BabaYagaRunner(); +}); + +// Add some helpful console commands +window.babaYagaRunnerCommands = { + run: () => window.babaYagaRunner?.runCode(), + format: () => window.babaYagaRunner?.formatCode(), + getCode: () => window.babaYagaRunner?.getCode(), + loadSample: () => window.babaYagaRunner?.loadSampleCode(), + clearOutput: () => window.babaYagaRunner?.clearOutput() +}; diff --git a/js/baba-yaga/web/editor/js/editor.js b/js/baba-yaga/web/editor/js/editor.js new file mode 100644 index 0000000..37ab24f --- /dev/null +++ b/js/baba-yaga/web/editor/js/editor.js @@ -0,0 +1,1004 @@ +/** + * BabaYagaEditor - Main editor class for the structural editor + * Manages the text editor, structural editor, and AST synchronization + */ +class BabaYagaEditor { + constructor(container) { + this.container = container; + this.parser = null; + this.tree = null; + this.editor = null; + this.astSynchronizer = null; + this.checkLanguageModeInterval = null; + + this.init(); + } + + async init() { + try { + await this.initTreeSitter(); + this.initEditor(); + this.initASTSynchronizer(); + this.bindEvents(); + this.loadSampleCode(); + } catch (error) { + console.error('Failed to initialize editor:', error); + this.showError('Failed to initialize editor: ' + error.message); + } + } + + async initTreeSitter() { + // Initialize tree-sitter parser (optional for now) + if (typeof TreeSitter !== 'undefined') { + this.parser = new TreeSitter(); + try { + const JavaScript = await TreeSitter.Language.load('tree-sitter-javascript.wasm'); + this.parser.setLanguage(JavaScript); + } catch (error) { + console.warn('Could not load JavaScript grammar, tree-sitter parsing disabled'); + } + } else { + console.warn('Tree-sitter not loaded, using Baba Yaga parser instead'); + } + } + + initEditor() { + // Initialize CodeMirror editor + const textarea = this.container.querySelector('#code-editor'); + if (!textarea) { + throw new Error('Code editor textarea not found'); + } + + // Check if CodeMirror is available + if (typeof CodeMirror !== 'undefined') { + console.log('CodeMirror is available, checking for Baba Yaga language mode...'); + console.log('Available modes:', Object.keys(CodeMirror.modes)); + + // Check if Baba Yaga language mode is available (CodeMirror 5) + const mode = CodeMirror.modes['baba-yaga']; + + if (mode) { + console.log('Baba Yaga language mode found, initializing with syntax highlighting'); + // Initialize CodeMirror with Baba Yaga language mode + this.editor = CodeMirror.fromTextArea(textarea, { + mode: 'baba-yaga', + theme: 'baba-yaga', // Use our custom theme + lineNumbers: true, + autoCloseBrackets: true, + matchBrackets: true, + indentUnit: 2, + tabSize: 2, + indentWithTabs: false, + lineWrapping: true, + foldGutter: true, + gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'], + extraKeys: { + 'Tab': 'indentMore', + 'Shift-Tab': 'indentLess', + 'Enter': 'newlineAndIndent' + } + }); + + // Ensure CodeMirror fills the container properly + setTimeout(() => { + this.editor.refresh(); + this.forceRefreshEditor(); + }, 100); + } else { + console.warn('Baba Yaga language mode not found, using plain text mode'); + console.log('Will retry when language mode becomes available...'); + + // Initialize CodeMirror with plain text mode + this.editor = CodeMirror.fromTextArea(textarea, { + mode: 'text', + theme: 'monokai', + lineNumbers: true, + autoCloseBrackets: true, + matchBrackets: true, + indentUnit: 2, + tabSize: 2, + indentWithTabs: false, + lineWrapping: true, + foldGutter: true, + gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'], + extraKeys: { + 'Tab': 'indentMore', + 'Shift-Tab': 'indentLess', + 'Enter': 'newlineAndIndent' + } + }); + + // Listen for the language mode to become available + window.addEventListener('baba-yaga-mode-ready', () => { + console.log('Baba Yaga language mode ready event received!'); + this.retryLanguageMode(); + }); + } + + // Set up change listener with debouncing for auto-parsing + let parseTimeout; + this.editor.on('change', () => { + // Clear previous timeout + if (parseTimeout) { + clearTimeout(parseTimeout); + } + + // Set new timeout for auto-parsing (500ms delay) + parseTimeout = setTimeout(() => { + if (this.astSynchronizer) { + this.astSynchronizer.updateAST(); + } + }, 500); + }); + + } else { + // Fallback to basic textarea if CodeMirror is not available + console.warn('CodeMirror not available, using basic textarea'); + this.editor = { + getValue: () => textarea.value, + setValue: (value) => { textarea.value = value; }, + on: (event, callback) => { + if (event === 'change') { + textarea.addEventListener('input', callback); + } + } + }; + + // Set up change listener with debouncing for auto-parsing + let parseTimeout; + this.editor.on('change', () => { + // Clear previous timeout + if (parseTimeout) { + clearTimeout(parseTimeout); + } + + // Set new timeout for auto-parsing (500ms delay) + parseTimeout = setTimeout(() => { + if (this.astSynchronizer) { + this.astSynchronizer.updateAST(); + } + }, 500); + }); + } + + // Set up add button event handlers + this.setupAddButtons(); + + // Set up retry syntax highlighting button + this.setupRetryButton(); + + // Set up window resize handler for CodeMirror + this.setupResizeHandler(); + + // Periodically check if Baba Yaga language mode becomes available + if (typeof CodeMirror !== 'undefined') { + this.checkLanguageModeInterval = setInterval(() => { + if (this.retryLanguageMode()) { + clearInterval(this.checkLanguageModeInterval); + } + }, 1000); // Check every second + } + } + + initASTSynchronizer() { + this.astSynchronizer = new ASTSynchronizer(this, null); + } + + bindEvents() { + // Parse button + const parseBtn = this.container.querySelector('#parse-btn'); + if (parseBtn) { + parseBtn.addEventListener('click', () => this.parseCode()); + } + + // Format button + const formatBtn = this.container.querySelector('#format-btn'); + if (formatBtn) { + formatBtn.addEventListener('click', () => this.formatCode()); + } + + // Run button + const runBtn = this.container.querySelector('#run-btn'); + if (runBtn) { + runBtn.addEventListener('click', () => this.runCode()); + } + + // Tab switching + const tabBtns = this.container.querySelectorAll('.structural-tabs .tab-btn'); + tabBtns.forEach(btn => { + btn.addEventListener('click', () => this.switchTab(btn.dataset.tab)); + }); + + // Output tab switching + const outputTabBtns = this.container.querySelectorAll('.output-tabs .tab-btn'); + outputTabBtns.forEach(btn => { + btn.addEventListener('click', () => this.switchOutputTab(btn.dataset.tab)); + }); + } + + switchTab(tabName) { + // Hide all tab panes + const tabPanes = this.container.querySelectorAll('.tab-pane'); + tabPanes.forEach(pane => pane.classList.remove('active')); + + // Remove active class from all tab buttons + const tabBtns = this.container.querySelectorAll('.structural-tabs .tab-btn'); + tabBtns.forEach(btn => btn.classList.remove('active')); + + // Show selected tab pane + const selectedPane = this.container.querySelector(`#${tabName}-tab`); + if (selectedPane) { + selectedPane.classList.add('active'); + } + + // Activate selected tab button + const selectedBtn = this.container.querySelector(`[data-tab="${tabName}"]`); + if (selectedBtn) { + selectedBtn.classList.add('active'); + } + } + + switchOutputTab(tabName) { + // Hide all output tab panes + const outputTabPanes = this.container.querySelectorAll('.output-content .tab-pane'); + outputTabPanes.forEach(pane => pane.classList.remove('active')); + + // Remove active class from all output tab buttons + const outputTabBtns = this.container.querySelectorAll('.output-tabs .tab-btn'); + outputTabBtns.forEach(btn => btn.classList.remove('active')); + + // Show selected output tab pane + const selectedPane = this.container.querySelector(`#${tabName}-tab`); + if (selectedPane) { + selectedPane.classList.add('active'); + } + + // Activate selected output tab button + const selectedBtn = this.container.querySelector(`[data-tab="${tabName}"]`); + if (selectedBtn) { + selectedBtn.classList.add('active'); + } + } + + parseCode() { + try { + const code = this.editor.getValue(); + if (!code.trim()) { + this.showError('No code to parse'); + return; + } + + let ast; + try { + // Try to use the real Baba Yaga parser + ast = this.parseWithBabaYaga(code); + } catch (parserError) { + console.warn('Real parser failed, falling back to basic parsing:', parserError); + // Fallback to basic parsing + ast = this.basicParse(code); + } + + this.tree = ast; + + // Update the tree view + this.updateTreeView(ast); + + // No longer using structural editors + + this.showSuccess('Code parsed successfully'); + this.showASTJSON(ast); + + } catch (error) { + this.showError('Parse error: ' + error.message); + console.error('Parse error:', error); + } + } + + parseWithBabaYaga(code) { + try { + // Check if Baba Yaga components are available + console.log('Checking Baba Yaga components...'); + console.log('createLexer:', typeof createLexer); + console.log('createParser:', typeof createParser); + console.log('createInterpreter:', typeof createInterpreter); + + if (typeof createLexer === 'undefined' || typeof createParser === 'undefined') { + throw new Error('Baba Yaga language components not loaded'); + } + + // Use the real Baba Yaga lexer and parser + const lexer = createLexer(code); + const tokens = lexer.allTokens(); + console.log('Tokens generated:', tokens.length); + + const parser = createParser(tokens); + const ast = parser.parse(); + console.log('AST generated:', ast); + + return ast; + } catch (error) { + console.error('Baba Yaga parsing error:', error); + throw new Error(`Parsing failed: ${error.message}`); + } + } + + basicParse(code) { + // Basic parsing for demonstration purposes + // This will be replaced with proper tree-sitter parsing + const lines = code.split('\n').filter(line => line.trim()); + const ast = { + type: 'Program', + body: [] + }; + + lines.forEach((line, index) => { + const trimmed = line.trim(); + if (trimmed && !trimmed.startsWith('//')) { + if (trimmed.includes(':')) { + const [name, ...rest] = trimmed.split(':'); + const value = rest.join(':').trim(); + + if (value.includes('->')) { + // Function declaration + ast.body.push({ + type: 'FunctionDeclaration', + name: name.trim(), + params: this.parseFunctionParams(value), + body: this.parseFunctionBody(value), + line: index + 1 + }); + } else { + // Variable declaration + ast.body.push({ + type: 'VariableDeclaration', + name: name.trim(), + value: value, + line: index + 1 + }); + } + } + } + }); + + return ast; + } + + parseFunctionParams(value) { + // Basic function parameter parsing + const arrowIndex = value.indexOf('->'); + if (arrowIndex === -1) return []; + + const beforeArrow = value.substring(0, arrowIndex).trim(); + if (!beforeArrow) return []; + + // Simple space-separated parameter parsing + return beforeArrow.split(/\s+/).filter(p => p.trim()); + } + + parseFunctionBody(value) { + // Basic function body parsing + const arrowIndex = value.indexOf('->'); + if (arrowIndex === -1) return ''; + + return value.substring(arrowIndex + 2).trim(); + } + + updateTreeView(ast) { + const treeView = this.container.querySelector('#tree-view'); + if (!treeView) return; + + treeView.innerHTML = this.renderTree(ast); + } + + renderTree(node, depth = 0) { + const indent = ' '.repeat(depth); + let html = ''; + + if (!node || !node.type) { + return html; + } + + // Create a unique ID for this node + const nodeId = `node-${Math.random().toString(36).substr(2, 9)}`; + + html += `${indent}<div class="tree-node" data-node-id="${nodeId}" data-node-type="${node.type}">`; + html += `<div class="tree-node-content">`; + + // Node type (always editable) + html += `<span class="tree-node-type tree-node-editable" ondblclick="window.babaYagaEditor?.editNodeType('${nodeId}', '${node.type}')">${node.type}</span>`; + + // Node name (if exists) + if (node.name !== undefined) { + html += `<span class="tree-node-value tree-node-editable" ondblclick="window.babaYagaEditor?.editNodeValue('${nodeId}', 'name', '${node.name}')">${node.name}</span>`; + } + + // Node value (if exists) + if (node.value !== undefined) { + html += `<span class="tree-node-value tree-node-editable" ondblclick="window.babaYagaEditor?.editNodeValue('${nodeId}', 'value', '${JSON.stringify(node.value)}')">: ${JSON.stringify(node.value)}</span>`; + } + + // Handle different node types + if (node.params && Array.isArray(node.params)) { + html += `<span class="tree-node-value"> (${node.params.length} params)</span>`; + } + + if (node.returnType) { + html += `<span class="tree-node-value tree-node-editable" ondblclick="window.babaYagaEditor?.editNodeValue('${nodeId}', 'value', 'returnType', '${node.returnType}')"> -> ${node.returnType}</span>`; + } + + // Action buttons + html += `<div class="tree-node-actions">`; + html += `<button class="tree-node-action-btn" onclick="window.babaYagaEditor?.addChildNode('${nodeId}', '${node.type}')">+</button>`; + html += `<button class="tree-node-action-btn delete" onclick="window.babaYagaEditor?.deleteNode('${nodeId}')">×</button>`; + html += `</div>`; + + html += `</div>`; + + // Render children + if (node.body && Array.isArray(node.body)) { + html += `<div class="tree-node-children">`; + node.body.forEach(child => { + html += this.renderTree(child, depth + 1); + }); + html += `</div>`; + } else if (node.body && typeof node.body === 'object') { + html += `<div class="tree-node-children">`; + html += this.renderTree(node.body, depth + 1); + html += `</div>`; + } + + // Handle other child properties + if (node.params && Array.isArray(node.params)) { + html += `<div class="tree-node-children">`; + html += `<div class="tree-node"><span class="tree-node-type">params</span></div>`; + node.params.forEach((param, index) => { + if (typeof param === 'string') { + html += `<div class="tree-node-children"><div class="tree-node"><span class="tree-node-value tree-node-editable" ondblclick="window.babaYagaEditor?.editParamValue('${nodeId}', ${index}, '${param}')">${param}</span></div></div>`; + } else if (param.name) { + html += `<div class="tree-node-children"><div class="tree-node"><span class="tree-node-value tree-node-editable" ondblclick="window.babaYagaEditor?.editParamValue('${nodeId}', ${index}, '${param.name}', '${param.type || ''}')">${param.name}${param.type ? ': ' + param.type : ''}</span></div></div>`; + } + }); + html += `</div>`; + } + + html += `</div>`; + + return html; + } + + formatCode() { + try { + const code = this.editor.getValue(); + if (!code.trim()) { + this.showError('No code to format'); + return; + } + + // Basic formatting - in the future this will be more sophisticated + const formatted = this.basicFormat(code); + this.editor.setValue(formatted); + + this.showSuccess('Code formatted successfully'); + + } catch (error) { + this.showError('Format error: ' + error.message); + } + } + + basicFormat(code) { + // Basic code formatting + const lines = code.split('\n'); + const formatted = []; + let indentLevel = 0; + + lines.forEach(line => { + const trimmed = line.trim(); + if (!trimmed) { + formatted.push(''); + return; + } + + // Decrease indent for closing braces + if (trimmed === '}' || trimmed === ']' || trimmed === ')') { + indentLevel = Math.max(0, indentLevel - 1); + } + + // Add indentation + const indent = ' '.repeat(indentLevel); + formatted.push(indent + trimmed); + + // Increase indent for opening braces + if (trimmed === '{' || trimmed === '[' || trimmed === '(') { + indentLevel++; + } + }); + + return formatted.join('\n'); + } + + async runCode() { + try { + const code = this.editor.getValue(); + if (!code.trim()) { + this.showError('No code to run'); + return; + } + + // Parse the code first + let ast; + try { + ast = this.parseWithBabaYaga(code); + } catch (parserError) { + ast = this.basicParse(code); + } + + // Check if interpreter is available + if (typeof createInterpreter === 'undefined') { + this.showOutput('Interpreter not available. Code parsed successfully:\n' + JSON.stringify(ast, null, 2)); + return; + } + + // Execute using the Baba Yaga interpreter + const interpreter = createInterpreter(ast); + const result = interpreter.interpret(); + + // Display the result + this.showOutput(`Code executed successfully!\nResult: ${JSON.stringify(result, null, 2)}`); + + } catch (error) { + this.showError('Execution error: ' + error.message); + console.error('Execution error:', error); + } + } + + loadSampleCode() { + const sampleCode = `// Sample Baba Yaga code - demonstrating syntax highlighting +add : x y -> x + y; + +multiply : x y -> x * y; + +// Simple function +double : x -> x * 2; + +// Basic arithmetic +calculate : x y -> (x + y) * 2;`; + + if (this.editor && this.editor.setValue) { + this.editor.setValue(sampleCode); + } + } + + showOutput(message) { + const outputText = this.container.querySelector('#output-text'); + if (outputText) { + outputText.textContent = message; + } + } + + showError(message) { + const errorsText = this.container.querySelector('#errors-text'); + if (errorsText) { + errorsText.textContent = message; + errorsText.className = 'error'; + } + } + + showSuccess(message) { + const outputText = this.container.querySelector('#output-text'); + if (outputText) { + outputText.textContent = message; + outputText.className = 'success'; + } + } + + showASTJSON(ast) { + const astJsonText = this.container.querySelector('#ast-json-text'); + if (astJsonText) { + astJsonText.textContent = JSON.stringify(ast, null, 2); + } + } + + getAST() { + return this.tree; + } + + getCode() { + return this.editor.getValue(); + } + + selectASTNode(nodeId, nodeData) { + console.log('Selected AST node:', nodeId, nodeData); + + // Highlight the selected node + this.highlightSelectedNode(nodeId); + + // Populate the structural editor based on node type + this.populateStructuralEditor(nodeData); + + // Switch to the appropriate tab + this.switchToStructuralTab(nodeData.type); + } + + highlightSelectedNode(nodeId) { + // Remove previous highlights + document.querySelectorAll('.tree-node.selected').forEach(node => { + node.classList.remove('selected'); + }); + + // Add highlight to selected node + const selectedNode = document.querySelector(`[data-node-id="${nodeId}"]`); + if (selectedNode) { + selectedNode.classList.add('selected'); + } + } + + populateStructuralEditor(nodeData) { + // No longer using structural editors - inline editing is handled directly + console.log('Selected node for inline editing:', nodeData); + } + + switchToStructuralTab(nodeType) { + let tabName = 'function'; // default + + switch (nodeType) { + case 'FunctionDeclaration': + tabName = 'function'; + break; + case 'WhenExpression': + tabName = 'when'; + break; + case 'WithHeader': + tabName = 'with'; + break; + } + + this.switchTab(tabName); + } + + // Inline AST editing methods + editNodeType(nodeId, currentType) { + const nodeElement = document.querySelector(`[data-node-id="${nodeId}"]`); + if (!nodeElement) return; + + const typeElement = nodeElement.querySelector('.tree-node-type'); + const input = document.createElement('input'); + input.type = 'text'; + input.value = currentType; + input.className = 'tree-node-editing'; + + input.onblur = () => this.finishEditNodeType(nodeId, input.value); + input.onkeydown = (e) => { + if (e.key === 'Enter') { + this.finishEditNodeType(nodeId, input.value); + } else if (e.key === 'Escape') { + this.cancelEdit(nodeElement, typeElement); + } + }; + + typeElement.innerHTML = ''; + typeElement.appendChild(input); + input.focus(); + } + + editNodeValue(nodeId, property, currentValue) { + const nodeElement = document.querySelector(`[data-node-id="${nodeId}"]`); + if (!nodeElement) return; + + const valueElement = nodeElement.querySelector(`[ondblclick*="${property}"]`); + if (!valueElement) return; + + const input = document.createElement('input'); + input.type = 'text'; + input.value = currentValue; + input.className = 'tree-node-editing'; + + input.onblur = () => this.finishEditNodeValue(nodeId, property, input.value); + input.onkeydown = (e) => { + if (e.key === 'Enter') { + this.finishEditNodeValue(nodeId, property, input.value); + } else if (e.key === 'Escape') { + this.cancelEdit(nodeElement, valueElement); + } + }; + + valueElement.innerHTML = ''; + valueElement.appendChild(input); + input.focus(); + } + + editParamValue(nodeId, paramIndex, currentName, currentType = '') { + const nodeElement = document.querySelector(`[data-node-id="${nodeId}"]`); + if (!nodeElement) return; + + const paramElements = nodeElement.querySelectorAll('.tree-node-children .tree-node-value'); + const paramElement = paramElements[paramIndex]; + if (!paramElement) return; + + const input = document.createElement('input'); + input.type = 'text'; + input.value = currentType ? `${currentName}: ${currentType}` : currentName; + input.className = 'tree-node-editing'; + + input.onblur = () => this.finishEditParamValue(nodeId, paramIndex, input.value); + input.onkeydown = (e) => { + if (e.key === 'Enter') { + this.finishEditParamValue(nodeId, paramIndex, input.value); + } else if (e.key === 'Escape') { + this.cancelEdit(nodeElement, paramElement); + } + }; + + paramElement.innerHTML = ''; + paramElement.appendChild(input); + input.focus(); + } + + finishEditNodeType(nodeId, newType) { + // Find the node in the AST and update it + this.updateASTNodeProperty(nodeId, 'type', newType); + this.refreshTreeView(); + } + + finishEditNodeValue(nodeId, property, newValue) { + // Find the node in the AST and update it + this.updateASTNodeProperty(nodeId, property, newValue); + this.refreshTreeView(); + } + + finishEditParamValue(nodeId, paramIndex, newValue) { + // Parse the new parameter value + let name, type; + if (newValue.includes(':')) { + [name, type] = newValue.split(':').map(s => s.trim()); + } else { + name = newValue; + type = null; + } + + // Find the node in the AST and update the parameter + this.updateASTNodeParam(nodeId, paramIndex, name, type); + this.refreshTreeView(); + } + + cancelEdit(nodeElement, originalElement) { + // Restore the original content + this.refreshTreeView(); + } + + addChildNode(nodeId, parentType) { + // Add a new child node based on parent type + const newNode = this.createDefaultChildNode(parentType); + this.addChildToASTNode(nodeId, newNode); + this.refreshTreeView(); + } + + deleteNode(nodeId) { + // Remove the node from the AST + this.removeASTNode(nodeId); + this.refreshTreeView(); + } + + createDefaultChildNode(parentType) { + switch (parentType) { + case 'Program': + return { type: 'FunctionDeclaration', name: 'newFunction', params: [], body: 'expression' }; + case 'FunctionDeclaration': + return { type: 'WhenExpression', discriminants: [], cases: [] }; + case 'WithHeader': + return { type: 'WithHeader', recursive: false, entries: [], body: 'expression' }; + default: + return { type: 'Expression', value: 'newValue' }; + } + } + + updateASTNodeProperty(nodeId, property, value) { + // This is a simplified implementation + // In a real implementation, you'd traverse the AST to find and update the node + console.log(`Updating node ${nodeId} property ${property} to ${value}`); + + // Update the code editor to reflect changes + this.syncASTToCode(); + } + + updateASTNodeParam(nodeId, paramIndex, name, type) { + console.log(`Updating node ${nodeId} parameter ${paramIndex} to ${name}: ${type}`); + this.syncASTToCode(); + } + + addChildToASTNode(nodeId, childNode) { + console.log(`Adding child to node ${nodeId}:`, childNode); + this.syncASTToCode(); + } + + removeASTNode(nodeId) { + console.log(`Removing node ${nodeId}`); + this.syncASTToCode(); + } + + syncASTToCode() { + // Generate code from the current AST and update the code editor + if (this.astSynchronizer) { + const newCode = this.astSynchronizer.generateCode(this.tree); + this.editor.setValue(newCode); + } + } + + refreshTreeView() { + // Re-render the tree view with the updated AST + if (this.tree) { + this.updateTreeView(this.tree); + } + } + + refreshEditor() { + // Refresh CodeMirror editor if it's available + if (this.editor && this.editor.refresh) { + this.editor.refresh(); + } + } + + // Force refresh the editor layout + forceRefreshEditor() { + if (this.editor) { + // Force a complete refresh + this.editor.refresh(); + + // Also trigger a resize event to ensure proper layout + setTimeout(() => { + if (this.editor.refresh) { + this.editor.refresh(); + } + }, 50); + } + } + + retryLanguageMode() { + // Try to reload the Baba Yaga language mode + if (typeof CodeMirror !== 'undefined' && this.editor) { + // For CodeMirror 5, we can directly check if the mode is available + if (CodeMirror.modes['baba-yaga']) { + console.log('Baba Yaga language mode now available, switching to it'); + this.editor.setOption('mode', 'baba-yaga'); + // Force refresh after mode change + setTimeout(() => { + this.forceRefreshEditor(); + }, 50); + return true; + } + } + return false; + } + + cleanup() { + // Clean up intervals + if (this.checkLanguageModeInterval) { + clearInterval(this.checkLanguageModeInterval); + this.checkLanguageModeInterval = null; + } + } + + setupAddButtons() { + // Add Function button + const addFunctionBtn = document.getElementById('add-function-btn'); + if (addFunctionBtn) { + addFunctionBtn.addEventListener('click', () => { + this.addNewFunction(); + }); + } + + // Add When button + const addWhenBtn = document.getElementById('add-when-btn'); + if (addWhenBtn) { + addWhenBtn.addEventListener('click', () => { + this.addNewWhen(); + }); + } + + // Add With button + const addWithBtn = document.getElementById('add-with-btn'); + if (addWithBtn) { + addWithBtn.addEventListener('click', () => { + this.addNewWith(); + }); + } + } + + setupRetryButton() { + const retryBtn = document.getElementById('retry-syntax-btn'); + if (retryBtn) { + retryBtn.addEventListener('click', () => { + console.log('Manual retry of syntax highlighting...'); + if (this.retryLanguageMode()) { + retryBtn.textContent = '✅'; + retryBtn.style.backgroundColor = '#4ec9b0'; + setTimeout(() => { + retryBtn.textContent = '🔄'; + retryBtn.style.backgroundColor = '#6a9955'; + }, 2000); + } else { + retryBtn.textContent = '❌'; + retryBtn.style.backgroundColor = '#f14c4c'; + setTimeout(() => { + retryBtn.textContent = '🔄'; + retryBtn.style.backgroundColor = '#6a9955'; + }, 2000); + } + }); + } + } + + setupResizeHandler() { + // Handle window resize to ensure CodeMirror fills container + window.addEventListener('resize', () => { + if (this.editor && this.editor.refresh) { + setTimeout(() => { + this.editor.refresh(); + }, 100); + } + }); + + // Also handle when the container becomes visible + const observer = new ResizeObserver(() => { + if (this.editor && this.editor.refresh) { + setTimeout(() => { + this.editor.refresh(); + }, 100); + } + }); + + if (this.container) { + observer.observe(this.container); + } + } + + addNewFunction() { + const newFunction = { + type: 'FunctionDeclaration', + name: 'newFunction', + params: [], + body: 'expression', + returnType: null + }; + + if (!this.tree) { + this.tree = { type: 'Program', body: [] }; + } + + this.tree.body.push(newFunction); + this.refreshTreeView(); + this.syncASTToCode(); + } + + addNewWhen() { + const newWhen = { + type: 'WhenExpression', + discriminants: [], + cases: [] + }; + + if (!this.tree) { + this.tree = { type: 'Program', body: [] }; + } + + this.tree.body.push(newWhen); + this.refreshTreeView(); + this.syncASTToCode(); + } + + addNewWith() { + const newWith = { + type: 'WithHeader', + recursive: false, + entries: [], + body: 'expression' + }; + + if (!this.tree) { + this.tree = { type: 'Program', body: [] }; + } + + this.tree.body.push(newWith); + this.refreshTreeView(); + this.syncASTToCode(); + } +} diff --git a/js/baba-yaga/web/editor/js/formatter.js b/js/baba-yaga/web/editor/js/formatter.js new file mode 100644 index 0000000..b0485d6 --- /dev/null +++ b/js/baba-yaga/web/editor/js/formatter.js @@ -0,0 +1,621 @@ +/** + * Browser-compatible Baba Yaga code formatter + * Adapted from fmt.js for use in the web editor + */ + +class BabaYagaFormatter { + constructor(options = {}) { + this.indentSize = options.indentSize || 2; + this.maxLineLength = options.maxLineLength || 100; + this.preserveComments = options.preserveComments !== false; + } + + /** + * Format source code string + */ + format(source) { + try { + if (typeof createLexer === 'undefined' || typeof createParser === 'undefined') { + throw new Error('Baba Yaga language components not loaded'); + } + + const lexer = createLexer(source); + const tokens = lexer.allTokens(); + + // Extract comments before parsing + const comments = this.extractComments(source); + + const parser = createParser(tokens); + const ast = parser.parse(); + + return this.formatAST(ast, comments, source); + } catch (error) { + throw new Error(`Formatting failed: ${error.message}`); + } + } + + /** + * Extract comments from source with their positions + */ + extractComments(source) { + const comments = []; + const lines = source.split('\n'); + + lines.forEach((line, lineIndex) => { + const commentMatch = line.match(/\/\/(.*)$/); + if (commentMatch) { + const column = line.indexOf('//'); + comments.push({ + line: lineIndex + 1, + column: column, + text: commentMatch[0], + content: commentMatch[1].trim() + }); + } + }); + + return comments; + } + + /** + * Format AST node + */ + formatAST(ast, comments = [], originalSource = '') { + return this.visitNode(ast, 0, comments); + } + + /** + * Visit and format a node + */ + visitNode(node, depth = 0, comments = []) { + if (!node) return ''; + + switch (node.type) { + case 'Program': + return this.formatProgram(node, depth, comments); + case 'TypeDeclaration': + return this.formatTypeDeclaration(node, depth); + case 'VariableDeclaration': + return this.formatVariableDeclaration(node, depth, comments); + case 'FunctionDeclaration': + return this.formatFunctionDeclaration(node, depth, comments); + case 'CurriedFunctionDeclaration': + return this.formatCurriedFunctionDeclaration(node, depth, comments); + case 'WithHeader': + return this.formatWithHeader(node, depth, comments); + case 'WhenExpression': + return this.formatWhenExpression(node, depth, comments); + case 'BinaryExpression': + return this.formatBinaryExpression(node, depth, comments); + case 'UnaryExpression': + return this.formatUnaryExpression(node, depth, comments); + case 'FunctionCall': + return this.formatFunctionCall(node, depth, comments); + case 'AnonymousFunction': + return this.formatAnonymousFunction(node, depth, comments); + case 'ListLiteral': + return this.formatListLiteral(node, depth, comments); + case 'TableLiteral': + return this.formatTableLiteral(node, depth, comments); + case 'MemberExpression': + return this.formatMemberExpression(node, depth, comments); + case 'ResultExpression': + return this.formatResultExpression(node, depth, comments); + case 'NumberLiteral': + return this.formatNumberLiteral(node); + case 'StringLiteral': + return this.formatStringLiteral(node); + case 'BooleanLiteral': + return this.formatBooleanLiteral(node); + case 'Identifier': + return this.formatIdentifier(node); + default: + // Fallback for unknown node types - avoid infinite recursion + if (typeof node === 'string') { + return node; + } + if (typeof node === 'number') { + return node.toString(); + } + if (typeof node === 'boolean') { + return node.toString(); + } + if (node && typeof node === 'object') { + // Try to handle as a literal value + if (node.value !== undefined) { + return node.value.toString(); + } + if (node.name !== undefined) { + return node.name; + } + } + return JSON.stringify(node); + } + } + + /** + * Format program (top level) + */ + formatProgram(node, depth, comments) { + const statements = []; + let lastWasFunction = false; + + node.body.forEach((stmt, index) => { + const formatted = this.visitNode(stmt, depth, comments); + const isFunction = stmt.type === 'FunctionDeclaration' || + stmt.type === 'CurriedFunctionDeclaration'; + + // Add extra spacing between functions and other statements + if (index > 0 && (isFunction || lastWasFunction)) { + statements.push(''); + } + + statements.push(formatted); + lastWasFunction = isFunction; + }); + + return statements.join('\n') + (statements.length > 0 ? '\n' : ''); + } + + /** + * Format type declaration + */ + formatTypeDeclaration(node, depth) { + const indent = this.getIndent(depth); + return `${indent}${node.name} ${node.typeAnnotation};`; + } + + /** + * Format variable declaration + */ + formatVariableDeclaration(node, depth, comments) { + const indent = this.getIndent(depth); + + // Check if the value is a complex expression that should be on its own line + if (node.value.type === 'WhenExpression' || node.value.type === 'WithHeader') { + const value = this.visitNode(node.value, depth + 1, comments); + return `${indent}${node.name} :\n${value};`; + } else { + const value = this.visitNode(node.value, depth, comments); + return `${indent}${node.name} : ${value};`; + } + } + + /** + * Format function declaration + */ + formatFunctionDeclaration(node, depth, comments) { + const indent = this.getIndent(depth); + let result = `${indent}${node.name} : `; + + // Format parameters + if (node.params && node.params.length > 0) { + if (this.hasTypedParams(node.params)) { + result += this.formatTypedParameters(node.params); + } else { + result += node.params.map(p => typeof p === 'string' ? p : p.name).join(' '); + } + } + + // Add return type if present + if (node.returnType) { + result += ` -> ${this.formatType(node.returnType)}`; + } + + result += ' ->\n'; + + // Format body with proper indentation + const body = this.visitNode(node.body, depth + 1, comments); + // If the body doesn't start with indentation, add it + if (body && !body.startsWith(this.getIndent(depth + 1))) { + result += this.getIndent(depth + 1) + body; + } else { + result += body; + } + + result += ';'; + return result; + } + + /** + * Format curried function declaration + */ + formatCurriedFunctionDeclaration(node, depth, comments) { + const indent = this.getIndent(depth); + let result = `${indent}${node.name} : `; + + // Format first typed parameter + result += `(${node.param.name}: ${this.formatType(node.param.type)})`; + + // Format return type + if (node.returnType) { + result += ` -> ${this.formatType(node.returnType)}`; + } + + result += ' ->\n'; + + // Format body with proper indentation + const body = this.visitNode(node.body, depth + 1, comments); + result += body + ';'; + + return result; + } + + /** + * Format with header + */ + formatWithHeader(node, depth, comments) { + const indent = this.getIndent(depth); + let result = `${indent}with`; + + if (node.recursive) { + result += ' rec'; + } + + result += ' (\n'; + + // Format entries + node.entries.forEach((entry, index) => { + const entryIndent = this.getIndent(depth + 1); + if (entry.type === 'WithTypeDecl') { + result += `${entryIndent}${entry.name} ${this.formatType(entry.typeAnnotation)};`; + } else if (entry.type === 'WithAssign') { + const value = this.visitNode(entry.value, depth + 1, comments); + result += `${entryIndent}${entry.name} : ${value};`; + } + + if (index < node.entries.length - 1) { + result += '\n'; + } + }); + + result += `\n${indent}) ->\n`; + const body = this.visitNode(node.body, depth + 1, comments); + // Ensure the body is properly indented + if (body && !body.startsWith(this.getIndent(depth + 1))) { + result += this.getIndent(depth + 1) + body; + } else { + result += body; + } + + return result; + } + + /** + * Format when expression + */ + formatWhenExpression(node, depth, comments) { + const indent = this.getIndent(depth); + let result = `${indent}when `; + + // Format discriminants + const discriminants = node.discriminants.map(d => + this.visitNode(d, 0, comments) + ).join(' '); + result += `${discriminants} is\n`; + + // Calculate the maximum pattern width to align 'then' keywords + const caseIndent = this.getIndent(depth + 1); + const formattedCases = node.cases.map(caseNode => { + const patterns = caseNode.patterns.map(p => + this.formatPattern(p, depth + 1, comments) + ).join(' '); + return { + patterns, + consequent: caseNode.consequent, + originalCase: caseNode + }; + }); + + // Find the maximum pattern length for alignment + const maxPatternLength = Math.max( + ...formattedCases.map(c => c.patterns.length) + ); + + // Format cases with aligned 'then' keywords + formattedCases.forEach((formattedCase, index) => { + const { patterns, consequent } = formattedCase; + + // Pad patterns to align 'then' keywords + const paddedPatterns = patterns.padEnd(maxPatternLength); + result += `${caseIndent}${paddedPatterns} then `; + + // Format consequent - handle nested when expressions specially + if (consequent.type === 'WhenExpression') { + // For nested when expressions, add newline and proper indentation + result += '\n' + this.visitNode(consequent, depth + 2, comments); + } else { + // For simple consequents, add inline + const consequentFormatted = this.visitNode(consequent, 0, comments); + result += consequentFormatted; + } + + // Add newline between cases (but not after the last one) + if (index < formattedCases.length - 1) { + result += '\n'; + } + }); + + return result; + } + + /** + * Format pattern + */ + formatPattern(pattern, depth, comments) { + if (!pattern) return ''; + + switch (pattern.type) { + case 'WildcardPattern': + return '_'; + case 'TypePattern': + return pattern.name; + case 'ResultPattern': + return `${pattern.variant} ${pattern.identifier.name}`; + case 'ListPattern': + const elements = pattern.elements.map(e => + this.formatPattern(e, depth, comments) + ).join(', '); + return `[${elements}]`; + case 'TablePattern': + const properties = pattern.properties.map(prop => + `${prop.key}: ${this.formatPattern(prop.value, depth, comments)}` + ).join(', '); + return `{${properties}}`; + case 'NumberLiteral': + return pattern.value.toString(); + case 'StringLiteral': + return `"${pattern.value}"`; + case 'BooleanLiteral': + return pattern.value.toString(); + case 'Identifier': + return pattern.name; + default: + // For literal patterns, try to format them directly + if (typeof pattern === 'string') { + return pattern; + } + if (typeof pattern === 'number') { + return pattern.toString(); + } + return this.visitNode(pattern, depth, comments); + } + } + + /** + * Format binary expression + */ + formatBinaryExpression(node, depth, comments) { + const left = this.visitNode(node.left, depth, comments); + const right = this.visitNode(node.right, depth, comments); + + // Add spaces around operators + const needsSpaces = !['.', '..'].includes(node.operator); + if (needsSpaces) { + return `${left} ${node.operator} ${right}`; + } else { + return `${left}${node.operator}${right}`; + } + } + + /** + * Format unary expression + */ + formatUnaryExpression(node, depth, comments) { + const operand = this.visitNode(node.operand, depth, comments); + return `${node.operator}${operand}`; + } + + /** + * Format function call + */ + formatFunctionCall(node, depth, comments) { + const callee = this.visitNode(node.callee, depth, comments); + const args = node.arguments.map(arg => + this.visitNode(arg, depth, comments) + ); + + if (args.length === 0) { + return callee; + } + + // Handle parentheses for complex expressions + const formattedArgs = args.map(arg => { + // If argument contains operators or is complex, wrap in parentheses + if (arg.includes(' -> ') || (arg.includes(' ') && !arg.startsWith('"') && !arg.startsWith('['))) { + return `(${arg})`; + } + return arg; + }); + + return `${callee} ${formattedArgs.join(' ')}`; + } + + /** + * Format anonymous function + */ + formatAnonymousFunction(node, depth, comments) { + // Handle both string parameters and object parameters + const params = node.params.map(param => { + if (typeof param === 'string') { + return param; + } else if (param && typeof param === 'object' && param.name) { + return param.name; + } else if (param && typeof param === 'object' && param.type === 'Identifier') { + return param.name; + } else { + return String(param); + } + }).join(' '); + const body = this.visitNode(node.body, depth, comments); + return `${params} -> ${body}`; + } + + /** + * Format list literal + */ + formatListLiteral(node, depth, comments) { + if (node.elements.length === 0) { + return '[]'; + } + + const elements = node.elements.map(el => + this.visitNode(el, depth, comments) + ); + + // Single line if short, multi-line if long + const singleLine = `[${elements.join(', ')}]`; + if (singleLine.length <= 50) { + return singleLine; + } + + const indent = this.getIndent(depth); + const elementIndent = this.getIndent(depth + 1); + let result = '[\n'; + elements.forEach((el, index) => { + result += `${elementIndent}${el}`; + if (index < elements.length - 1) { + result += ','; + } + result += '\n'; + }); + result += `${indent}]`; + return result; + } + + /** + * Format table literal + */ + formatTableLiteral(node, depth, comments) { + if (node.properties.length === 0) { + return '{}'; + } + + const properties = node.properties.map(prop => { + const value = this.visitNode(prop.value, depth + 1, comments); + return `${prop.key}: ${value}`; + }); + + // Single line if short, multi-line if long + const singleLine = `{${properties.join(', ')}}`; + if (singleLine.length <= 50 && !properties.some(p => p.includes('\n'))) { + return singleLine; + } + + const indent = this.getIndent(depth); + const propIndent = this.getIndent(depth + 1); + let result = '{\n'; + properties.forEach((prop, index) => { + result += `${propIndent}${prop}`; + if (index < properties.length - 1) { + result += ','; + } + result += '\n'; + }); + result += `${indent}}`; + return result; + } + + /** + * Format member expression + */ + formatMemberExpression(node, depth, comments) { + const object = this.visitNode(node.object, depth, comments); + const property = this.visitNode(node.property, depth, comments); + return `${object}.${property}`; + } + + /** + * Format result expression + */ + formatResultExpression(node, depth, comments) { + const value = this.visitNode(node.value, depth, comments); + return `${node.variant} ${value}`; + } + + /** + * Format number literal + */ + formatNumberLiteral(node) { + return node.value.toString(); + } + + /** + * Format string literal + */ + formatStringLiteral(node) { + return `"${node.value}"`; + } + + /** + * Format boolean literal + */ + formatBooleanLiteral(node) { + return node.value.toString(); + } + + /** + * Format identifier + */ + formatIdentifier(node) { + return node.name; + } + + // Helper methods + + /** + * Get indentation string + */ + getIndent(depth) { + return ' '.repeat(depth * this.indentSize); + } + + /** + * Check if parameters have type annotations + */ + hasTypedParams(params) { + return params.some(p => + typeof p === 'object' && p.type && p.type !== 'Identifier' + ); + } + + /** + * Format typed parameters + */ + formatTypedParameters(params) { + const formatted = params.map(p => { + if (typeof p === 'string') { + return p; + } else if (p.type && p.type !== 'Identifier') { + return `${p.name}: ${this.formatType(p.type)}`; + } else { + return p.name; + } + }); + return `(${formatted.join(', ')})`; + } + + /** + * Format type annotation + */ + formatType(type) { + if (typeof type === 'string') { + return type; + } + + if (type.type === 'PrimitiveType') { + return type.name; + } + + if (type.type === 'FunctionType') { + const paramTypes = type.paramTypes.map(t => this.formatType(t)).join(', '); + const returnType = this.formatType(type.returnType); + return `(${paramTypes}) -> ${returnType}`; + } + + return 'Unknown'; + } +} + +// Make formatter available globally +window.BabaYagaFormatter = BabaYagaFormatter; diff --git a/js/baba-yaga/web/editor/js/main.js b/js/baba-yaga/web/editor/js/main.js new file mode 100644 index 0000000..29dda7c --- /dev/null +++ b/js/baba-yaga/web/editor/js/main.js @@ -0,0 +1,225 @@ +/** + * main.js - Main entry point for the Baba Yaga Structural Editor + * Initializes the editor when the page loads + */ + +// Wait for DOM to be ready +document.addEventListener('DOMContentLoaded', () => { + console.log('Baba Yaga Structural Editor initializing...'); + + try { + // Initialize the main editor + const container = document.querySelector('.editor-container'); + if (!container) { + throw new Error('Editor container not found'); + } + + // Create and initialize the editor + const editor = new BabaYagaEditor(container); + + // Store reference globally for debugging + window.babaYagaEditor = editor; + + console.log('Baba Yaga Structural Editor initialized successfully'); + + // Add some helpful console commands for development + window.babaYagaEditorCommands = { + getAST: () => editor.getAST(), + getCode: () => editor.getCode(), + parse: () => editor.parseCode(), + format: () => editor.formatCode(), + run: () => editor.runCode(), + retryLanguageMode: () => editor.retryLanguageMode(), + initLanguageMode: () => window.initBabaYagaMode(), + forceRefresh: () => editor.forceRefreshEditor(), + refresh: () => editor.refreshEditor(), + showAST: () => { + const ast = editor.getAST(); + console.log('Current AST:', ast); + return ast; + }, + showCode: () => { + const code = editor.getCode(); + console.log('Current Code:', code); + return code; + } + }; + + console.log('Development commands available: window.babaYagaEditorCommands'); + + } catch (error) { + console.error('Failed to initialize Baba Yaga Structural Editor:', error); + + // Show error message to user + const errorDiv = document.createElement('div'); + errorDiv.style.cssText = ` + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background: #d73a49; + color: white; + padding: 2rem; + border-radius: 8px; + font-family: monospace; + max-width: 80%; + text-align: center; + z-index: 10000; + `; + errorDiv.innerHTML = ` + <h2>Initialization Error</h2> + <p>${error.message}</p> + <p>Check the console for more details.</p> + <button onclick="this.parentElement.remove()" style="margin-top: 1rem; padding: 0.5rem 1rem; border: none; border-radius: 4px; cursor: pointer;">Close</button> + `; + document.body.appendChild(errorDiv); + } +}); + +// Add some global error handling +window.addEventListener('error', (event) => { + console.error('Global error:', event.error); +}); + +window.addEventListener('unhandledrejection', (event) => { + console.error('Unhandled promise rejection:', event.reason); +}); + +// Add some helpful utility functions +window.babaYagaUtils = { + // Parse Baba Yaga code manually + parseCode: (code) => { + try { + // Try to use the real Baba Yaga parser if available + if (typeof createLexer !== 'undefined' && typeof createParser !== 'undefined') { + try { + const lexer = createLexer(code); + const tokens = lexer.allTokens(); + const parser = createParser(tokens); + return parser.parse(); + } catch (parserError) { + console.warn('Real parser failed, falling back to basic parsing:', parserError); + } + } + + // Basic parsing for demonstration + const lines = code.split('\n').filter(line => line.trim()); + const ast = { + type: 'Program', + body: [] + }; + + lines.forEach((line, index) => { + const trimmed = line.trim(); + if (trimmed && !trimmed.startsWith('//')) { + if (trimmed.includes(':')) { + const [name, ...rest] = trimmed.split(':'); + const value = rest.join(':').trim(); + + if (value.includes('->')) { + ast.body.push({ + type: 'FunctionDeclaration', + name: name.trim(), + params: window.babaYagaUtils.parseFunctionParams(value), + body: window.babaYagaUtils.parseFunctionBody(value), + line: index + 1 + }); + } else { + ast.body.push({ + type: 'VariableDeclaration', + name: name.trim(), + value: value, + line: index + 1 + }); + } + } + } + }); + + return ast; + } catch (error) { + console.error('Parse error:', error); + throw error; + } + }, + + parseFunctionParams: (value) => { + const arrowIndex = value.indexOf('->'); + if (arrowIndex === -1) return []; + + const beforeArrow = value.substring(0, arrowIndex).trim(); + if (!beforeArrow) return []; + + return beforeArrow.split(/\s+/).filter(p => p.trim()); + }, + + parseFunctionBody: (value) => { + const arrowIndex = value.indexOf('->'); + if (arrowIndex === -1) return ''; + + return value.substring(arrowIndex + 2).trim(); + }, + + // Generate code from AST + generateCode: (ast) => { + if (!ast || ast.type !== 'Program') return ''; + + const lines = []; + + ast.body.forEach(node => { + switch (node.type) { + case 'FunctionDeclaration': + lines.push(window.babaYagaUtils.generateFunctionCode(node)); + break; + case 'VariableDeclaration': + lines.push(window.babaYagaUtils.generateVariableCode(node)); + break; + default: + lines.push(`// Unknown node type: ${node.type}`); + } + }); + + return lines.join('\n'); + }, + + generateFunctionCode: (node) => { + let code = `${node.name} : `; + + if (node.params && node.params.length > 0) { + code += node.params.join(' '); + } + + code += ' -> '; + code += node.body || ''; + + return code; + }, + + generateVariableCode: (node) => { + return `${node.name} : ${node.value};`; + }, + + // Validate Baba Yaga syntax + validateSyntax: (code) => { + try { + const ast = window.babaYagaUtils.parseCode(code); + return { valid: true, ast }; + } catch (error) { + return { valid: false, error: error.message }; + } + }, + + // Format Baba Yaga code + formatCode: (code) => { + try { + const ast = window.babaYagaUtils.parseCode(code); + return window.babaYagaUtils.generateCode(ast); + } catch (error) { + console.error('Format error:', error); + return code; // Return original code if formatting fails + } + } +}; + +console.log('Baba Yaga utilities available: window.babaYagaUtils'); +console.log('Try: window.babaYagaUtils.parseCode("add : x y -> x + y;")'); diff --git a/js/baba-yaga/web/editor/js/structural-editors.js b/js/baba-yaga/web/editor/js/structural-editors.js new file mode 100644 index 0000000..3203d19 --- /dev/null +++ b/js/baba-yaga/web/editor/js/structural-editors.js @@ -0,0 +1,501 @@ +/** + * StructuralEditors - Manages all structural editing panels + * Provides interfaces for editing functions, when expressions, and with headers + */ +class StructuralEditors { + constructor(container) { + this.container = container; + this.changeCallbacks = []; + this.currentAST = null; + this.silentUpdate = false; + + this.init(); + } + + init() { + this.initFunctionEditor(); + this.initWhenEditor(); + this.initWithEditor(); + this.bindEvents(); + } + + initFunctionEditor() { + this.functionEditor = { + name: this.container.querySelector('#func-name'), + params: this.container.querySelector('#func-params'), + returnType: this.container.querySelector('#func-return-type'), + body: this.container.querySelector('#func-body'), + addParamBtn: this.container.querySelector('#add-param-btn') + }; + } + + initWhenEditor() { + this.whenEditor = { + discriminants: this.container.querySelector('#when-discriminants'), + cases: this.container.querySelector('#when-cases'), + addDiscriminantBtn: this.container.querySelector('#add-discriminant-btn'), + addCaseBtn: this.container.querySelector('#add-case-btn') + }; + } + + initWithEditor() { + this.withEditor = { + recursive: this.container.querySelector('#with-recursive'), + entries: this.container.querySelector('#with-entries'), + body: this.container.querySelector('#with-body'), + addEntryBtn: this.container.querySelector('#add-entry-btn') + }; + } + + bindEvents() { + // Function editor events + if (this.functionEditor.addParamBtn) { + this.functionEditor.addParamBtn.addEventListener('click', () => { + this.addFunctionParameter(); + }); + } + + if (this.functionEditor.name) { + this.functionEditor.name.addEventListener('input', () => { + this.onFunctionChange(); + }); + } + + if (this.functionEditor.returnType) { + this.functionEditor.returnType.addEventListener('change', () => { + this.onFunctionChange(); + }); + } + + // When editor events + if (this.whenEditor.addDiscriminantBtn) { + this.whenEditor.addDiscriminantBtn.addEventListener('click', () => { + this.addWhenDiscriminant(); + }); + } + + if (this.whenEditor.addCaseBtn) { + this.whenEditor.addCaseBtn.addEventListener('click', () => { + this.addWhenCase(); + }); + } + + // With editor events + if (this.withEditor.addEntryBtn) { + this.withEditor.addEntryBtn.addEventListener('click', () => { + this.addWithEntry(); + }); + } + + if (this.withEditor.recursive) { + this.withEditor.recursive.addEventListener('change', () => { + this.onWithChange(); + }); + } + } + + // Function Editor Methods + + addFunctionParameter() { + const paramItem = document.createElement('div'); + paramItem.className = 'parameter-item'; + paramItem.innerHTML = ` + <input type="text" class="param-name" placeholder="parameterName" /> + <select class="param-type"> + <option value="">Infer</option> + <option value="Int">Int</option> + <option value="Float">Float</option> + <option value="String">String</option> + <option value="Bool">Bool</option> + <option value="List">List</option> + <option value="Table">Table</option> + </select> + <button class="remove-param-btn" onclick="this.parentElement.remove()">Remove</button> + `; + + // Add event listeners for the new parameter + const nameInput = paramItem.querySelector('.param-name'); + const typeSelect = paramItem.querySelector('.param-type'); + + nameInput.addEventListener('input', () => this.onFunctionChange()); + typeSelect.addEventListener('change', () => this.onFunctionChange()); + + this.functionEditor.params.appendChild(paramItem); + this.onFunctionChange(); + } + + onFunctionChange() { + // Don't notify changes during silent updates + if (this.silentUpdate) { + return; + } + + const changes = [{ + type: 'function_update', + oldName: this.getCurrentFunctionName(), + newName: this.functionEditor.name.value, + params: this.getFunctionParameters(), + returnType: this.functionEditor.returnType.value || null, + body: this.getFunctionBody() + }]; + + this.notifyChanges(changes); + } + + getCurrentFunctionName() { + // Try to get the name from the current AST selection + // For now, return a default + return 'currentFunction'; + } + + getFunctionParameters() { + const params = []; + const paramItems = this.functionEditor.params.querySelectorAll('.parameter-item'); + + paramItems.forEach(item => { + const name = item.querySelector('.param-name').value; + const type = item.querySelector('.param-type').value; + + if (name.trim()) { + params.push({ + name: name.trim(), + type: type || null + }); + } + }); + + return params; + } + + getFunctionBody() { + // For now, return a simple expression + // In the future, this will be a more sophisticated expression builder + return 'expression'; + } + + // When Editor Methods + + addWhenDiscriminant() { + const discriminantItem = document.createElement('div'); + discriminantItem.className = 'expression-item'; + discriminantItem.innerHTML = ` + <input type="text" class="discriminant-expr" placeholder="expression" /> + <button class="remove-discriminant-btn" onclick="this.parentElement.remove()">Remove</button> + `; + + const exprInput = discriminantItem.querySelector('.discriminant-expr'); + exprInput.addEventListener('input', () => this.onWhenChange()); + + this.whenEditor.discriminants.appendChild(discriminantItem); + this.onWhenChange(); + } + + addWhenCase() { + const caseItem = document.createElement('div'); + caseItem.className = 'case-item'; + caseItem.innerHTML = ` + <div class="case-header"> + <h4>Case</h4> + <button class="remove-case-btn" onclick="this.closest('.case-item').remove()">Remove</button> + </div> + <div class="case-patterns"> + <div class="pattern-item"> + <select class="pattern-type"> + <option value="literal">Literal</option> + <option value="type">Type</option> + <option value="wildcard">Wildcard</option> + <option value="list">List</option> + <option value="table">Table</option> + </select> + <input type="text" class="pattern-value" placeholder="pattern value" /> + <button class="add-pattern-btn" onclick="this.parentElement.parentElement.appendChild(this.parentElement.cloneNode(true))">Add Pattern</button> + </div> + </div> + <div class="case-consequent"> + <label>Consequent:</label> + <input type="text" class="consequent-expr" placeholder="expression" /> + </div> + `; + + // Add event listeners + const patternType = caseItem.querySelector('.pattern-type'); + const patternValue = caseItem.querySelector('.pattern-value'); + const consequentExpr = caseItem.querySelector('.consequent-expr'); + + patternType.addEventListener('change', () => this.onWhenChange()); + patternValue.addEventListener('input', () => this.onWhenChange()); + consequentExpr.addEventListener('input', () => this.onWhenChange()); + + this.whenEditor.cases.appendChild(caseItem); + this.onWhenChange(); + } + + onWhenChange() { + // Don't notify changes during silent updates + if (this.silentUpdate) { + return; + } + + const changes = [{ + type: 'when_update', + discriminants: this.getWhenDiscriminants(), + cases: this.getWhenCases() + }]; + + this.notifyChanges(changes); + } + + getWhenDiscriminants() { + const discriminants = []; + const discriminantItems = this.whenEditor.discriminants.querySelectorAll('.discriminant-expr'); + + discriminantItems.forEach(item => { + const value = item.value.trim(); + if (value) { + discriminants.push(value); + } + }); + + return discriminants; + } + + getWhenCases() { + const cases = []; + const caseItems = this.whenEditor.cases.querySelectorAll('.case-item'); + + caseItems.forEach(item => { + const patterns = []; + const patternItems = item.querySelectorAll('.pattern-item'); + + patternItems.forEach(patternItem => { + const type = patternItem.querySelector('.pattern-type').value; + const value = patternItem.querySelector('.pattern-value').value.trim(); + + if (value) { + patterns.push({ type, value }); + } + }); + + const consequent = item.querySelector('.consequent-expr').value.trim(); + + if (patterns.length > 0 && consequent) { + cases.push({ patterns, consequent }); + } + }); + + return cases; + } + + // With Editor Methods + + addWithEntry() { + const entryItem = document.createElement('div'); + entryItem.className = 'entry-item'; + entryItem.innerHTML = ` + <div class="entry-header"> + <h4>Entry</h4> + <button class="remove-entry-btn" onclick="this.closest('.entry-item').remove()">Remove</button> + </div> + <div class="entry-content"> + <div class="form-group"> + <label>Type:</label> + <select class="entry-type-selector"> + <option value="assignment">Assignment</option> + <option value="type-decl">Type Declaration</option> + </select> + </div> + <div class="form-group"> + <label>Name:</label> + <input type="text" class="entry-name" placeholder="variableName" /> + </div> + <div class="form-group entry-value-group"> + <label>Value:</label> + <input type="text" class="entry-value" placeholder="value or type" /> + </div> + </div> + `; + + // Add event listeners + const typeSelector = entryItem.querySelector('.entry-type-selector'); + const nameInput = entryItem.querySelector('.entry-name'); + const valueInput = entryItem.querySelector('.entry-value'); + + typeSelector.addEventListener('change', () => this.onWithChange()); + nameInput.addEventListener('input', () => this.onWithChange()); + valueInput.addEventListener('input', () => this.onWithChange()); + + this.withEditor.entries.appendChild(entryItem); + this.onWithChange(); + } + + onWithChange() { + // Don't notify changes during silent updates + if (this.silentUpdate) { + return; + } + + const changes = [{ + type: 'with_update', + recursive: this.withEditor.recursive.checked, + entries: this.getWithEntries(), + body: this.getWithBody() + }]; + + this.notifyChanges(changes); + } + + getWithEntries() { + const entries = []; + const entryItems = this.withEditor.entries.querySelectorAll('.entry-item'); + + entryItems.forEach(item => { + const type = item.querySelector('.entry-type-selector').value; + const name = item.querySelector('.entry-name').value.trim(); + const value = item.querySelector('.entry-value').value.trim(); + + if (name && value) { + entries.push({ type, name, value }); + } + }); + + return entries; + } + + getWithBody() { + // For now, return a simple expression + // In the future, this will be a more sophisticated expression builder + return 'expression'; + } + + // AST Integration Methods + + updateFromAST(ast, silent = false) { + this.currentAST = ast; + + // If this is a silent update, don't trigger change notifications + if (silent) { + this.silentUpdate = true; + } + + // Find the first function declaration to populate the function editor + if (ast && ast.body) { + const firstFunction = ast.body.find(node => node.type === 'FunctionDeclaration'); + if (firstFunction) { + this.populateFunctionEditor(firstFunction); + } + + // Find when expressions and with headers in function bodies + this.populateWhenAndWithEditors(ast); + } + + // Reset silent flag + if (silent) { + this.silentUpdate = false; + } + } + + populateFunctionEditor(functionNode) { + if (!this.functionEditor.name) return; + + this.functionEditor.name.value = functionNode.name || ''; + + // Clear existing parameters + this.functionEditor.params.innerHTML = ''; + + // Add parameters + if (functionNode.params) { + functionNode.params.forEach(param => { + this.addFunctionParameter(); + const lastParam = this.functionEditor.params.lastElementChild; + if (lastParam) { + const nameInput = lastParam.querySelector('.param-name'); + const typeSelect = lastParam.querySelector('.param-type'); + + if (typeof param === 'string') { + nameInput.value = param; + } else if (param.name) { + nameInput.value = param.name; + typeSelect.value = param.type || ''; + } else if (param.value) { + // Handle case where param might be an Identifier node + nameInput.value = param.value || ''; + } + } + }); + } + + // Set return type + if (this.functionEditor.returnType) { + this.functionEditor.returnType.value = functionNode.returnType || ''; + } + } + + populateWhenAndWithEditors(ast) { + // This is a simplified implementation + // In the future, this will properly parse and populate when expressions and with headers + console.log('Populating when and with editors from AST:', ast); + } + + populateWhenEditor(whenNode) { + if (!this.whenEditor.discriminants) return; + + // Clear existing content + this.whenEditor.discriminants.innerHTML = ''; + this.whenEditor.cases.innerHTML = ''; + + // TODO: Parse when expression and populate discriminants and cases + console.log('Populating when editor with:', whenNode); + } + + populateWithEditor(withNode) { + if (!this.withEditor.entries) return; + + // Clear existing content + this.withEditor.entries.innerHTML = ''; + + // Set recursive flag + if (this.withEditor.recursive) { + this.withEditor.recursive.checked = withNode.recursive || false; + } + + // TODO: Parse with header and populate entries + console.log('Populating with editor with:', withNode); + } + + // Change Notification + + onChange(callback) { + this.changeCallbacks.push(callback); + } + + notifyChanges(changes) { + this.changeCallbacks.forEach(callback => { + try { + callback(changes); + } catch (error) { + console.error('Error in change callback:', error); + } + }); + } + + // Utility Methods + + clearEditors() { + // Clear function editor + if (this.functionEditor.name) this.functionEditor.name.value = ''; + if (this.functionEditor.params) this.functionEditor.params.innerHTML = ''; + if (this.functionEditor.returnType) this.functionEditor.returnType.value = ''; + + // Clear when editor + if (this.whenEditor.discriminants) this.whenEditor.discriminants.innerHTML = ''; + if (this.whenEditor.cases) this.whenEditor.cases.innerHTML = ''; + + // Clear with editor + if (this.withEditor.entries) this.withEditor.entries.innerHTML = ''; + if (this.withEditor.recursive) this.withEditor.recursive.checked = false; + } + + getCurrentAST() { + return this.currentAST; + } +} diff --git a/js/baba-yaga/web/editor/js/tree-sitter-baba-yaga.js b/js/baba-yaga/web/editor/js/tree-sitter-baba-yaga.js new file mode 100644 index 0000000..8a2757d --- /dev/null +++ b/js/baba-yaga/web/editor/js/tree-sitter-baba-yaga.js @@ -0,0 +1,79 @@ +/** + * Tree-sitter Baba Yaga Grammar (Placeholder) + * + * This is a placeholder file that will be replaced with the actual tree-sitter grammar + * once we develop it. For now, it provides a basic structure that the editor can work with. + * + * The actual grammar will be generated from a .js file using tree-sitter-cli + */ + +// Placeholder grammar - this will be replaced with the actual compiled grammar +console.log('Tree-sitter Baba Yaga grammar placeholder loaded'); +console.log('This will be replaced with the actual grammar file'); + +// For now, we'll use basic parsing in the editor +// The actual tree-sitter integration will happen when we: +// 1. Create the grammar file (baba-yaga.js) +// 2. Compile it to WASM +// 3. Load it in the editor + +// Example of what the actual grammar might look like: +/* +module.exports = grammar({ + name: 'baba_yaga', + + rules: { + program: $ => repeat($.statement), + + statement: $ => choice( + $.type_declaration, + $.variable_declaration, + $.function_declaration, + $.expression_statement + ), + + function_declaration: $ => seq( + field('name', $.identifier), + ':', + choice( + $.typed_function_signature, + $.untyped_function_signature + ), + '->', + field('body', $.expression) + ), + + typed_function_signature: $ => seq( + '(', + commaSep($.typed_parameter), + ')', + optional(seq('->', $.type_annotation)) + ), + + when_expression: $ => seq( + 'when', + field('discriminants', commaSep($.expression)), + 'is', + repeat($.when_case) + ), + + with_header: $ => seq( + 'with', + optional('rec'), + '(', + repeat($.with_entry), + ')', + '->', + field('body', $.expression) + ) + } +}); +*/ + +// Export a placeholder object so the editor doesn't crash +if (typeof module !== 'undefined' && module.exports) { + module.exports = { + name: 'baba_yaga_placeholder', + rules: {} + }; +} diff --git a/js/baba-yaga/web/editor/structural.html b/js/baba-yaga/web/editor/structural.html new file mode 100644 index 0000000..169e696 --- /dev/null +++ b/js/baba-yaga/web/editor/structural.html @@ -0,0 +1,101 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>Baba Yaga Structural Editor - Advanced AST Editing</title> + <link rel="stylesheet" href="styles.css"> + <!-- CodeMirror for text editing --> + <link rel="stylesheet" href="../../node_modules/codemirror/lib/codemirror.css"> + <link rel="stylesheet" href="../../node_modules/codemirror/theme/monokai.css"> + <script src="../../node_modules/codemirror/lib/codemirror.js"></script> + <script src="../../node_modules/codemirror/addon/edit/closebrackets.js"></script> + <script src="../../node_modules/codemirror/addon/edit/matchbrackets.js"></script> + <script src="../../node_modules/codemirror/addon/fold/foldcode.js"></script> + <script src="../../node_modules/codemirror/addon/fold/foldgutter.js"></script> + <script src="../../node_modules/codemirror/addon/fold/brace-fold.js"></script> + <script src="../../node_modules/codemirror/addon/fold/indent-fold.js"></script> + <!-- Baba Yaga Language Components --> + <script type="module"> + // Import Baba Yaga components and make them globally available + import { createLexer, tokenTypes } from '../../lexer.js'; + import { createParser } from '../../parser.js'; + import { createInterpreter } from '../../interpreter.js'; + + // Make them globally available + window.createLexer = createLexer; + window.createParser = createParser; + window.createInterpreter = createInterpreter; + window.tokenTypes = tokenTypes; + + console.log('Baba Yaga modules loaded and made globally available'); + </script> + <!-- Baba Yaga Language Mode - Load after CodeMirror --> + <script src="js/baba-yaga-mode.js"></script> + <!-- Tree-sitter for parsing (optional for now) --> + <script src="https://unpkg.com/tree-sitter@0.20.6/dist/tree-sitter.js"></script> +</head> +<body> + <div class="editor-container"> + <!-- Header --> + <header class="editor-header"> + <h1>Baba Yaga Structural Editor</h1> + <div class="header-controls"> + <button id="parse-btn">Parse</button> + <button id="format-btn">Format</button> + <button id="run-btn">Run</button> + <a href="index.html" style="text-decoration: none; display: inline-block; background-color: #8b5cf6; color: white; padding: 0.5rem 1rem; border-radius: 4px; cursor: pointer; font-size: 0.9rem; transition: background-color 0.2s;" onmouseover="this.style.backgroundColor='#7c3aed'" onmouseout="this.style.backgroundColor='#8b5cf6'">▶ Code Runner</a> + </div> + </header> + + <!-- Main editor area --> + <div class="editor-main"> + <!-- Top row: Code editor and AST tree view side by side --> + <div class="top-row"> + <!-- Code editor (50% width) --> + <div class="code-editor-panel"> + <h3>Code Editor <span id="parse-status" class="parse-status"></span><button id="retry-syntax-btn" class="retry-btn" title="Retry loading syntax highlighting">🔄</button></h3> + <div class="code-editor-container"> + <textarea id="code-editor" placeholder="Enter your Baba Yaga code here..."></textarea> + </div> + </div> + + <!-- AST Tree View/Editor (50% width) --> + <div class="ast-editor-panel"> + <h3>AST Tree Editor <button id="add-function-btn" class="add-btn">+ Function</button><button id="add-when-btn" class="add-btn">+ When</button><button id="add-with-btn" class="add-btn">+ With</button></h3> + <div class="tree-editor-container"> + <div id="tree-view"></div> + </div> + </div> + </div> + + <!-- Bottom row: Output panel --> + <div class="output-panel"> + <h3>Output</h3> + <div class="output-tabs"> + <button class="tab-btn active" data-tab="output">Output</button> + <button class="tab-btn" data-tab="errors">Errors</button> + <button class="tab-btn" data-tab="ast-json">AST JSON</button> + </div> + <div class="output-content"> + <div id="output-tab" class="tab-pane active"> + <pre id="output-text"></pre> + </div> + <div id="errors-tab" class="tab-pane"> + <pre id="errors-text"></pre> + </div> + <div id="ast-json-tab" class="tab-pane"> + <pre id="ast-json-text"></pre> + </div> + </div> + </div> + </div> + </div> + + <!-- Scripts --> + <script src="js/tree-sitter-baba-yaga.js"></script> + <script src="js/editor.js"></script> + <script src="js/ast-synchronizer.js"></script> + <script src="js/main.js"></script> +</body> +</html> diff --git a/js/baba-yaga/web/editor/styles.css b/js/baba-yaga/web/editor/styles.css new file mode 100644 index 0000000..51983b9 --- /dev/null +++ b/js/baba-yaga/web/editor/styles.css @@ -0,0 +1,755 @@ +/* Reset and base styles */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + background-color: #1e1e1e; + color: #d4d4d4; + line-height: 1.6; +} + +/* Editor container */ +.editor-container { + display: flex; + flex-direction: column; + height: 100vh; + overflow: hidden; +} + +/* Header */ +.editor-header { + background-color: #2d2d30; + border-bottom: 1px solid #3e3e42; + padding: 1rem; + display: flex; + justify-content: space-between; + align-items: center; +} + +.editor-header h1 { + color: #569cd6; + font-size: 1.5rem; + font-weight: 600; +} + +.header-controls { + display: flex; + gap: 0.5rem; +} + +.header-controls button { + background-color: #007acc; + color: white; + border: none; + padding: 0.5rem 1rem; + border-radius: 4px; + cursor: pointer; + font-size: 0.9rem; + transition: background-color 0.2s; +} + +.header-controls button:hover { + background-color: #005a9e; +} + +/* Main editor area */ +.editor-main { + display: flex; + flex-direction: column; + flex: 1; + overflow: hidden; +} + +/* Top row: Code editor and AST tree view side by side */ +.top-row { + display: flex; + flex: 1; + overflow: hidden; +} + +/* Code editor panel (50% width) */ +.code-editor-panel { + flex: 1; + display: flex; + flex-direction: column; + border-right: 1px solid #3e3e42; + background-color: #1e1e1e; +} + +.code-editor-panel h3 { + padding: 1rem; + background-color: #2d2d30; + border-bottom: 1px solid #3e3e42; + color: #d4d4d4; +} + +.code-editor-container { + flex: 1; + overflow: hidden; + display: flex; + flex-direction: column; + min-height: 0; /* Important for flexbox to work properly */ +} + +#code-editor { + width: 100%; + height: 100%; + border: none; + outline: none; + background-color: #1e1e1e; + color: #d4d4d4; + font-family: 'Consolas', 'Monaco', 'Courier New', monospace; + font-size: 14px; + line-height: 1.5; + padding: 1rem; + resize: none; +} + +/* CodeMirror editor height fixes */ +.CodeMirror { + height: 100% !important; + min-height: 300px; + flex: 1; +} + +.CodeMirror-scroll { + min-height: 100%; +} + + + +/* AST Tree Editor panel (50% width) */ +.ast-editor-panel { + flex: 1; + display: flex; + flex-direction: column; + background-color: #252526; + border-left: 1px solid #3e3e42; +} + +.ast-editor-panel h3 { + padding: 1rem; + background-color: #2d2d30; + border-bottom: 1px solid #3e3e42; + color: #d4d4d4; + display: flex; + align-items: center; + justify-content: space-between; +} + +.tree-editor-container { + flex: 1; + overflow: auto; + padding: 1rem; +} + +/* Bottom row: Output panel */ +.output-panel { + height: 200px; + display: flex; + flex-direction: column; + background-color: #1e1e1e; + border-top: 1px solid #3e3e42; +} + +/* Tabs */ +.structural-tabs { + display: flex; + background-color: #2d2d30; + border-bottom: 1px solid #3e3e42; +} + +.tab-btn { + background-color: transparent; + color: #d4d4d4; + border: none; + padding: 0.75rem 1rem; + cursor: pointer; + border-bottom: 2px solid transparent; + transition: all 0.2s; +} + +.tab-btn:hover { + background-color: #3e3e42; +} + +.tab-btn.active { + background-color: #007acc; + color: white; + border-bottom-color: #007acc; +} + +/* Tab content */ +.tab-content { + flex: 1; + overflow: auto; + padding: 1rem; +} + +.tab-pane { + display: none; +} + +.tab-pane.active { + display: block; +} + +/* Tree view */ +.tree-view-container { + height: 100%; + overflow: auto; +} + +#tree-view { + font-family: 'Consolas', 'Monaco', 'Courier New', monospace; + font-size: 12px; + line-height: 1.4; +} + +.tree-node { + margin: 2px 0; + padding: 2px 0; +} + +.tree-node-content { + display: flex; + align-items: center; + cursor: pointer; + padding: 2px 4px; + border-radius: 3px; + transition: background-color 0.2s; +} + +.tree-node-content:hover { + background-color: #3e3e42; +} + +.tree-node-content.clickable { + cursor: pointer; +} + +.tree-node.selected .tree-node-content { + background-color: #007acc; + color: white; +} + +.tree-node.selected .tree-node-type { + color: white; +} + +.tree-node.selected .tree-node-value { + color: white; +} + +.tree-node-toggle { + margin-right: 8px; + color: #569cd6; + font-weight: bold; + width: 16px; + text-align: center; +} + +.tree-node-type { + color: #4ec9b0; + margin-right: 8px; + font-weight: 600; +} + +.tree-node-value { + color: #d4d4d4; +} + +.tree-node-children { + margin-left: 20px; + border-left: 1px solid #3e3e42; + padding-left: 10px; +} + +/* Form styles */ +.form-group { + margin-bottom: 1rem; +} + +.form-group label { + display: block; + margin-bottom: 0.5rem; + color: #d4d4d4; + font-weight: 500; +} + +.form-group input, +.form-group select { + width: 100%; + padding: 0.5rem; + border: 1px solid #3e3e42; + border-radius: 4px; + background-color: #1e1e1e; + color: #d4d4d4; + font-size: 14px; +} + +.form-group input:focus, +.form-group select:focus { + outline: none; + border-color: #007acc; + box-shadow: 0 0 0 2px rgba(0, 122, 204, 0.2); +} + +/* Parameter list */ +.parameter-list { + margin-bottom: 0.5rem; +} + +.parameter-item { + display: flex; + gap: 0.5rem; + margin-bottom: 0.5rem; + align-items: center; +} + +.parameter-item input { + flex: 1; +} + +.parameter-item select { + width: 120px; +} + +.remove-param-btn { + background-color: #d73a49; + color: white; + border: none; + padding: 0.25rem 0.5rem; + border-radius: 3px; + cursor: pointer; + font-size: 12px; +} + +.remove-param-btn:hover { + background-color: #b31d28; +} + +/* Expression builder */ +.expression-builder { + border: 1px solid #3e3e42; + border-radius: 4px; + padding: 1rem; + background-color: #1e1e1e; + min-height: 100px; +} + +.expression-item { + display: flex; + gap: 0.5rem; + margin-bottom: 0.5rem; + align-items: center; +} + +.expression-type-selector { + width: 120px; +} + +.expression-value { + flex: 1; +} + +/* When expression editor */ +.when-editor { + height: 100%; + overflow: auto; +} + +.expression-list, +.case-list { + margin-bottom: 1rem; +} + +.case-item { + border: 1px solid #3e3e42; + border-radius: 4px; + padding: 1rem; + margin-bottom: 1rem; + background-color: #1e1e1e; +} + +.case-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1rem; +} + +.case-patterns { + margin-bottom: 1rem; +} + +.pattern-item { + display: flex; + gap: 0.5rem; + margin-bottom: 0.5rem; + align-items: center; +} + +.remove-case-btn { + background-color: #d73a49; + color: white; + border: none; + padding: 0.25rem 0.5rem; + border-radius: 3px; + cursor: pointer; + font-size: 12px; +} + +/* With header editor */ +.with-editor { + height: 100%; + overflow: auto; +} + +.entry-list { + margin-bottom: 1rem; +} + +.entry-item { + border: 1px solid #3e3e42; + border-radius: 4px; + padding: 1rem; + margin-bottom: 1rem; + background-color: #1e1e1e; +} + +.entry-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1rem; +} + +.entry-type-selector { + width: 120px; +} + +.entry-name { + flex: 1; + margin-right: 0.5rem; +} + +.remove-entry-btn { + background-color: #d73a49; + color: white; + border: none; + padding: 0.25rem 0.5rem; + border-radius: 3px; + cursor: pointer; + font-size: 12px; +} + +/* Output panel */ +.output-panel { + height: 200px; + background-color: #252526; + border-top: 1px solid #3e3e42; +} + +.output-panel h3 { + padding: 1rem; + background-color: #2d2d30; + border-bottom: 1px solid #3e3e42; + color: #d4d4d4; +} + +.output-tabs { + display: flex; + background-color: #2d2d30; + border-bottom: 1px solid #3e3e42; +} + +.output-content { + flex: 1; + overflow: auto; + padding: 1rem; +} + +.output-content pre { + font-family: 'Consolas', 'Monaco', 'Courier New', monospace; + font-size: 12px; + line-height: 1.4; + color: #d4d4d4; + white-space: pre-wrap; + word-wrap: break-word; +} + +/* Buttons */ +button { + background-color: #007acc; + color: white; + border: none; + padding: 0.5rem 1rem; + border-radius: 4px; + cursor: pointer; + font-size: 0.9rem; + transition: background-color 0.2s; +} + +button:hover { + background-color: #005a9e; +} + +button:disabled { + background-color: #3e3e42; + color: #6a6a6a; + cursor: not-allowed; +} + +/* Utility classes */ +.hidden { + display: none !important; +} + +.error { + color: #f14c4c; +} + +.success { + color: #4ec9b0; +} + +.warning { + color: #dcdcaa; +} + +/* Responsive design */ +@media (max-width: 1200px) { + .top-row { + flex-direction: column; + } + + .code-editor-panel, + .ast-editor-panel { + flex: none; + height: 50%; + } +} + +@media (max-width: 768px) { + .editor-header { + flex-direction: column; + gap: 1rem; + align-items: stretch; + } + + .header-controls { + justify-content: center; + } + + .structural-tabs { + flex-wrap: wrap; + } + + .tab-btn { + flex: 1; + min-width: 80px; + } +} + +/* Parse status indicator */ +.parse-status { + font-size: 0.8rem; + font-weight: normal; + margin-left: 0.5rem; +} + +.parse-status.parsing { + color: #dcdcaa; +} + +.parse-status.parsed { + color: #4ec9b0; +} + +.parse-status.error { + color: #f14c4c; +} + +/* Baba Yaga Syntax Highlighting Enhancements */ +.CodeMirror { + font-family: 'Consolas', 'Monaco', 'Courier New', monospace; + font-size: 14px; + line-height: 1.5; +} + +/* Custom token colors for Baba Yaga - More specific selectors */ +.CodeMirror.cm-s-baba-yaga .cm-keyword, +.cm-s-baba-yaga .cm-keyword { + color: #c586c0 !important; /* Purple for keywords */ + font-weight: bold; +} + +.CodeMirror.cm-s-baba-yaga .cm-type, +.cm-s-baba-yaga .cm-type { + color: #4ec9b0 !important; /* Teal for types */ + font-weight: bold; +} + +.CodeMirror.cm-s-baba-yaga .cm-function, +.cm-s-baba-yaga .cm-function { + color: #dcdcaa !important; /* Yellow for functions */ + font-weight: bold; +} + +.CodeMirror.cm-s-baba-yaga .cm-builtin, +.cm-s-baba-yaga .cm-builtin { + color: #d7ba7d !important; /* Orange for builtins */ +} + +.CodeMirror.cm-s-baba-yaga .cm-operator, +.cm-s-baba-yaga .cm-operator { + color: #d4d4d4 !important; /* White for operators */ +} + +.CodeMirror.cm-s-baba-yaga .cm-number, +.cm-s-baba-yaga .cm-number { + color: #b5cea8 !important; /* Green for numbers */ +} + +.CodeMirror.cm-s-baba-yaga .cm-string, +.cm-s-baba-yaga .cm-string { + color: #ce9178 !important; /* Red for strings */ +} + +.CodeMirror.cm-s-baba-yaga .cm-comment, +.cm-s-baba-yaga .cm-comment { + color: #6a9955 !important; /* Green for comments */ + font-style: italic; +} + +.CodeMirror.cm-s-baba-yaga .cm-variable, +.cm-s-baba-yaga .cm-variable { + color: #9cdcfe !important; /* Blue for variables */ +} + +/* Dark theme adjustments for better contrast */ +.cm-s-baba-yaga.CodeMirror { + background-color: #1e1e1e; + color: #d4d4d4; +} + +.cm-s-baba-yaga .CodeMirror-gutters { + background-color: #2d2d30; + border-right: 1px solid #3e3e42; +} + +.cm-s-baba-yaga .CodeMirror-linenumber { + color: #858585; +} + +/* Focus state */ +.CodeMirror-focused .CodeMirror-cursor { + border-left: 2px solid #007acc; +} + +/* Selection */ +.CodeMirror-selected { + background-color: #264f78 !important; +} + +/* Active line highlighting */ +.CodeMirror-activeline-background { + background-color: #2d2d30; +} + +/* Add buttons */ +.add-btn { + background-color: #007acc; + color: white; + border: none; + padding: 0.25rem 0.5rem; + margin-left: 0.5rem; + border-radius: 3px; + cursor: pointer; + font-size: 0.8rem; + transition: background-color 0.2s; +} + +.add-btn:hover { + background-color: #005a9e; +} + +/* Retry button */ +.retry-btn { + background-color: #6a9955; + color: white; + border: none; + padding: 0.25rem 0.5rem; + margin-left: 0.5rem; + border-radius: 3px; + cursor: pointer; + font-size: 0.8rem; + transition: background-color 0.2s; +} + +.retry-btn:hover { + background-color: #4a7a35; +} + +.retry-btn:active { + transform: scale(0.95); +} + +/* Inline editing */ +.tree-node-editable { + cursor: pointer; + padding: 2px 4px; + border-radius: 3px; + transition: background-color 0.2s; +} + +.tree-node-editable:hover { + background-color: #3e3e42; +} + +.tree-node-editing { + background-color: #007acc; + color: white; +} + +.tree-node-editing input { + background: transparent; + border: none; + color: white; + outline: none; + font-family: inherit; + font-size: inherit; + width: 100%; +} + +.tree-node-actions { + display: inline-flex; + margin-left: 0.5rem; + gap: 0.25rem; +} + +.tree-node-action-btn { + background-color: #3e3e42; + color: #d4d4d4; + border: none; + padding: 1px 4px; + border-radius: 2px; + cursor: pointer; + font-size: 0.7rem; + transition: background-color 0.2s; +} + +.tree-node-action-btn:hover { + background-color: #007acc; + color: white; +} + +.tree-node-action-btn.delete:hover { + background-color: #f14c4c; +} + diff --git a/js/baba-yaga/web/editor/test-formatter.html b/js/baba-yaga/web/editor/test-formatter.html new file mode 100644 index 0000000..616afe2 --- /dev/null +++ b/js/baba-yaga/web/editor/test-formatter.html @@ -0,0 +1,155 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>Test Baba Yaga Formatter</title> + <style> + body { + font-family: monospace; + padding: 20px; + background: #1e1e1e; + color: #d4d4d4; + } + .test-section { + margin: 20px 0; + padding: 20px; + border: 1px solid #3e3e42; + border-radius: 8px; + } + .code-block { + background: #2d2d30; + padding: 10px; + border-radius: 4px; + white-space: pre-wrap; + margin: 10px 0; + } + .success { color: #4ec9b0; } + .error { color: #f14c4c; } + button { + background: #007acc; + color: white; + border: none; + padding: 10px 20px; + border-radius: 4px; + cursor: pointer; + margin: 10px 0; + } + button:hover { + background: #005a9e; + } + </style> +</head> +<body> + <h1>Baba Yaga Formatter Test</h1> + + <div class="test-section"> + <h2>Test 1: Basic Function Formatting</h2> + <div class="code-block" id="input1">add:x y->x+y;</div> + <button onclick="testFormat1()">Format Test 1</button> + <div class="code-block" id="output1"></div> + <div id="result1"></div> + </div> + + <div class="test-section"> + <h2>Test 2: Complex Code Formatting</h2> + <div class="code-block" id="input2">factorial:n->when n is 0 then 1 1 then 1 _ then n*factorial(n-1);</div> + <button onclick="testFormat2()">Format Test 2</button> + <div class="code-block" id="output2"></div> + <div id="result2"></div> + </div> + + <div class="test-section"> + <h2>Test 3: Multiple Functions</h2> + <div class="code-block" id="input3">add:x y->x+y; +multiply:x y->x*y; +result:add 5 3;</div> + <button onclick="testFormat3()">Format Test 3</button> + <div class="code-block" id="output3"></div> + <div id="result3"></div> + </div> + + <!-- Load Baba Yaga components --> + <script type="module"> + import { createLexer, tokenTypes } from '../../lexer.js'; + import { createParser } from '../../parser.js'; + + // Make them globally available + window.createLexer = createLexer; + window.createParser = createParser; + window.tokenTypes = tokenTypes; + + console.log('Baba Yaga modules loaded'); + </script> + + <!-- Load formatter --> + <script src="js/formatter.js"></script> + + <script> + function testFormat1() { + const input = document.getElementById('input1').textContent; + const output = document.getElementById('output1'); + const result = document.getElementById('result1'); + + try { + const formatter = new BabaYagaFormatter(); + const formatted = formatter.format(input); + output.textContent = formatted; + result.innerHTML = '<span class="success">✓ Formatting successful!</span>'; + } catch (error) { + output.textContent = 'Error: ' + error.message; + result.innerHTML = '<span class="error">✗ Formatting failed: ' + error.message + '</span>'; + } + } + + function testFormat2() { + const input = document.getElementById('input2').textContent; + const output = document.getElementById('output2'); + const result = document.getElementById('result2'); + + try { + const formatter = new BabaYagaFormatter(); + const formatted = formatter.format(input); + output.textContent = formatted; + result.innerHTML = '<span class="success">✓ Formatting successful!</span>'; + } catch (error) { + output.textContent = 'Error: ' + error.message; + result.innerHTML = '<span class="error">✗ Formatting failed: ' + error.message + '</span>'; + } + } + + function testFormat3() { + const input = document.getElementById('input3').textContent; + const output = document.getElementById('output3'); + const result = document.getElementById('result3'); + + try { + const formatter = new BabaYagaFormatter(); + const formatted = formatter.format(input); + output.textContent = formatted; + result.innerHTML = '<span class="success">✓ Formatting successful!</span>'; + } catch (error) { + output.textContent = 'Error: ' + error.message; + result.innerHTML = '<span class="error">✗ Formatting failed: ' + error.message + '</span>'; + } + } + + // Test formatter availability on load + window.addEventListener('load', () => { + setTimeout(() => { + if (typeof BabaYagaFormatter !== 'undefined') { + console.log('✓ BabaYagaFormatter is available'); + } else { + console.error('✗ BabaYagaFormatter is not available'); + } + + if (typeof createLexer !== 'undefined' && typeof createParser !== 'undefined') { + console.log('✓ Baba Yaga language components are available'); + } else { + console.error('✗ Baba Yaga language components are not available'); + } + }, 1000); + }); + </script> +</body> +</html> diff --git a/js/baba-yaga/web/editor/test-integration.html b/js/baba-yaga/web/editor/test-integration.html new file mode 100644 index 0000000..356b8cd --- /dev/null +++ b/js/baba-yaga/web/editor/test-integration.html @@ -0,0 +1,109 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>Baba Yaga Integration Test</title> +</head> +<body> + <h1>Baba Yaga Integration Test</h1> + + <div id="status">Loading...</div> + + <div id="test-results"></div> + + <!-- Baba Yaga Language Components --> + <script type="module"> + // Import Baba Yaga components and make them globally available + import { createLexer, tokenTypes } from '../../lexer.js'; + import { createParser } from '../../parser.js'; + import { createInterpreter } from '../../interpreter.js'; + + // Make them globally available + window.createLexer = createLexer; + window.createParser = createParser; + window.createInterpreter = createInterpreter; + window.tokenTypes = tokenTypes; + + console.log('Baba Yaga modules loaded and made globally available'); + </script> + + <script type="module"> + // Test the integration + async function testIntegration() { + const statusDiv = document.getElementById('status'); + const resultsDiv = document.getElementById('test-results'); + + try { + // Test 1: Check if modules are loaded + statusDiv.textContent = 'Testing module loading...'; + + if (typeof createLexer === 'undefined') { + throw new Error('createLexer not found'); + } + if (typeof createParser === 'undefined') { + throw new Error('createParser not found'); + } + if (typeof createInterpreter === 'undefined') { + throw new Error('createInterpreter not found'); + } + + resultsDiv.innerHTML += '<p>✅ All modules loaded successfully</p>'; + + // Test 2: Test lexer + statusDiv.textContent = 'Testing lexer...'; + const testCode = 'add : x y -> x + y;'; + const lexer = createLexer(testCode); + const tokens = lexer.allTokens(); + + resultsDiv.innerHTML += `<p>✅ Lexer working: ${tokens.length} tokens generated</p>`; + + // Test 3: Test parser + statusDiv.textContent = 'Testing parser...'; + const parser = createParser(tokens); + const ast = parser.parse(); + + resultsDiv.innerHTML += `<p>✅ Parser working: AST generated with ${ast.body.length} statements</p>`; + + // Test 4: Test interpreter + statusDiv.textContent = 'Testing interpreter...'; + const interpreter = createInterpreter(ast); + const result = interpreter.interpret(); + + resultsDiv.innerHTML += `<p>✅ Interpreter working: Result = ${JSON.stringify(result)}</p>`; + + // Test 5: Test more complex code + statusDiv.textContent = 'Testing complex code...'; + const complexCode = ` +factorial : n -> + when n is + 0 then 1 + _ then n * (factorial (n - 1)); + +result : factorial 5;`; + + const complexLexer = createLexer(complexCode); + const complexTokens = complexLexer.allTokens(); + const complexParser = createParser(complexTokens); + const complexAST = complexParser.parse(); + const complexInterpreter = createInterpreter(complexAST); + const complexResult = complexInterpreter.interpret(); + + resultsDiv.innerHTML += `<p>✅ Complex code working: Result = ${JSON.stringify(complexResult)}</p>`; + + statusDiv.textContent = 'All tests passed! 🎉'; + statusDiv.style.color = 'green'; + + } catch (error) { + statusDiv.textContent = 'Test failed! ❌'; + statusDiv.style.color = 'red'; + resultsDiv.innerHTML += `<p style="color: red;">❌ Error: ${error.message}</p>`; + console.error('Integration test failed:', error); + } + } + + // Run tests when page loads + document.addEventListener('DOMContentLoaded', testIntegration); + </script> +</body> +</html> diff --git a/js/baba-yaga/web/index.html b/js/baba-yaga/web/index.html new file mode 100644 index 0000000..fd20d11 --- /dev/null +++ b/js/baba-yaga/web/index.html @@ -0,0 +1,355 @@ +<!doctype html> +<html lang="en"> +<head> + <meta charset="utf-8" /> + <meta name="viewport" content="width=device-width, initial-scale=1" /> + <title>Baba Yaga REPL</title> + <style> + 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-info: #0066cc; + --color-code-bg: #f0f0f0; + --color-code-border: #ccc; + + font-family: system-ui, sans-serif; + background: var(--color-bg); + color: var(--color-text); + margin: 0; + padding: 0; + height: 100vh; + overflow: hidden; + } + + * { + box-sizing: border-box; + } + + /* Focus styles for accessibility */ + *:focus { + outline: 2px solid var(--color-button-bg); + outline-offset: 2px; + } + + /* Main layout */ + .app { + display: flex; + flex-direction: column; + height: 100vh; + } + + /* Chat container */ + .chat-container { + flex: 1; + display: flex; + flex-direction: column; + overflow: hidden; + max-width: 100%; + margin: 0 auto; + width: 100%; + } + + /* Messages area */ + .messages { + flex: 1; + overflow-y: auto; + padding: 1rem; + background: var(--color-main-bg); + } + + .message { + margin-bottom: 1rem; + } + + .message-input { + background: var(--color-code-bg); + border: 1.5px solid var(--color-code-border); + border-radius: 4px; + padding: 0.8rem; + margin-bottom: 0.5rem; + } + + .message-input .prompt { + color: var(--color-text); + font-family: monospace; + font-weight: bold; + font-size: 1em; + margin-bottom: 0.3em; + } + + .message-input .code { + font-family: monospace; + font-size: 1.25em; + color: var(--color-text); + white-space: pre-wrap; + word-wrap: break-word; + } + + .message-output { + background: var(--color-result-bg); + color: var(--color-text); + border: 1.75px solid var(--color-result-border); + border-radius: 4px; + padding: 0.8rem; + font-family: monospace; + font-size: 0.9em; + white-space: pre-wrap; + word-wrap: break-word; + } + + .message-error { + background: #fff0f0; + color: var(--color-error); + border: 1.75px solid var(--color-error); + border-radius: 4px; + padding: 0.8rem; + font-family: monospace; + font-size: 0.9em; + white-space: pre-wrap; + word-wrap: break-word; + } + + .message-info { + background: #f0f8ff; + color: var(--color-info); + border: 1.75px solid var(--color-info); + border-radius: 4px; + padding: 0.8rem; + font-size: 0.9em; + } + + /* Input area */ + .input-area { + background: var(--color-main-bg); + border-top: 2px solid var(--color-main-border); + padding: 1rem 1.5rem; + flex-shrink: 0; + } + + .input-container { + display: flex; + gap: 0.5rem; + align-items: flex-end; + margin-bottom: 0.5rem; + } + + .input-wrapper { + flex: 1; + } + + .input-textarea { + width: 100%; + background: var(--color-code-bg); + color: var(--color-text); + border: 2px solid var(--color-input-border); + border-radius: 4px; + padding: 0.6em; + font-family: monospace; + font-size: 1.25em; + line-height: 1.4; + resize: none; + outline: none; + min-height: 44px; + max-height: 120px; + overflow-y: auto; + } + + .input-textarea:focus { + border-color: var(--color-button-bg); + } + + .send-button { + background: var(--color-button-bg); + color: var(--color-button-text); + border: none; + border-radius: 4px; + padding: 0.6em 1.2em; + font-weight: bold; + text-transform: uppercase; + cursor: pointer; + font-size: 0.9em; + min-height: 44px; + min-width: 44px; + display: flex; + align-items: center; + justify-content: center; + } + + .send-button:hover { + opacity: 0.9; + } + + .send-button:active { + transform: translateY(1px); + } + + .send-button:disabled { + background: var(--color-button-disabled-bg); + cursor: not-allowed; + } + + /* Controls */ + .controls { + display: flex; + gap: 0.5rem; + flex-wrap: wrap; + } + + .button { + background: var(--color-button-bg); + color: var(--color-button-text); + border: none; + border-radius: 4px; + padding: 0.6em 1.2em; + font-weight: bold; + text-transform: uppercase; + cursor: pointer; + font-size: 0.8em; + min-height: 44px; + text-decoration: none; + display: inline-flex; + align-items: center; + justify-content: center; + } + + .button:hover { + opacity: 0.9; + } + + .button:active { + transform: translateY(1px); + } + + /* Mobile optimizations */ + @media (max-width: 768px) { + .header { + padding: 0.8rem 1rem; + } + + .header h1 { + font-size: 1.25rem; + } + + .messages { + padding: 0.8rem; + } + + .input-area { + padding: 0.8rem 1rem; + } + + .controls { + flex-direction: column; + } + + .button { + width: 100%; + } + } + + /* Dark mode support */ + @media (prefers-color-scheme: dark) { + body { + --color-bg: #0a0a0a; + --color-text: #ffffff; + --color-main-bg: #1a1a1a; + --color-main-border: #ffffff; + --color-shadow: #0003; + --color-label: #ffffff; + --color-input-border: #ffffff; + --color-button-bg: #ffffff; + --color-button-text: #000000; + --color-button-disabled-bg: #666666; + --color-result-border: #444444; + --color-result-bg: #2a2a2a; + --color-code-bg: #2a2a2a; + --color-code-border: #444444; + } + } + + /* High contrast mode */ + @media (prefers-contrast: high) { + body { + --color-bg: #000; + --color-text: #fff; + --color-main-bg: #000; + --color-main-border: #fff; + --color-button-bg: #fff; + --color-button-text: #000; + } + } + + /* Reduced motion */ + @media (prefers-reduced-motion: reduce) { + * { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + } + } + </style> +</head> +<body> + + <div class="app"> + + <main id="main" class="chat-container" role="main"> + <div class="messages" id="messages" role="log" aria-live="polite" aria-label="REPL conversation"></div> + + <div class="input-area"> + <div class="input-container"> + <div class="input-wrapper"> + <textarea + class="input-textarea" + id="input" + placeholder="Enter Baba Yaga code..." + aria-label="Code input" + rows="1" + ></textarea> + </div> + <button + class="send-button" + id="send" + type="button" + aria-label="Execute code" + > + Run + </button> + </div> + + <div class="controls"> + <button class="button" id="clear" type="button"> + Clear + </button> + <button class="button" id="load" type="button"> + Load + </button> + <button class="button" id="examples" type="button"> + Examples + </button> + <button class="button" id="help" type="button"> + Help + </button> + </div> + + <input id="fileInput" type="file" accept=".baba" style="display: none;" /> + </div> + </main> + </div> + + <script type="module" src="./app.js"></script> +</body> +</html> + |