From e6b88c9dec881382f457d5576011ea54e3d31a09 Mon Sep 17 00:00:00 2001
From: Sidi Liang <1467329765@qq.com>
Date: Tue, 5 Nov 2024 01:25:09 +0800
Subject: [PATCH] [web] Added Web REPL server and frontend demo
---
nasal-web-app/public/repl.html | 240 +++++++++++++++++++++++++++++++
nasal-web-app/public/repl.js | 249 +++++++++++++++++++++++++++++++++
nasal-web-app/server_repl.js | 176 +++++++++++++++++++++++
3 files changed, 665 insertions(+)
create mode 100644 nasal-web-app/public/repl.html
create mode 100644 nasal-web-app/public/repl.js
create mode 100644 nasal-web-app/server_repl.js
diff --git a/nasal-web-app/public/repl.html b/nasal-web-app/public/repl.html
new file mode 100644
index 0000000..9481424
--- /dev/null
+++ b/nasal-web-app/public/repl.html
@@ -0,0 +1,240 @@
+
+
+
+
+
+ Nasal Interpreter Web REPL
+
+
+
+
+
+
+
+
+
+
+
Welcome to Nasal Web REPL Demo!
+
+
+ >>>
+
+
+
+
+
+
+
+
+
+
Example Commands:
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/nasal-web-app/public/repl.js b/nasal-web-app/public/repl.js
new file mode 100644
index 0000000..742120e
--- /dev/null
+++ b/nasal-web-app/public/repl.js
@@ -0,0 +1,249 @@
+let replSessionId = null;
+let multilineInput = [];
+let historyIndex = -1;
+let commandHistory = [];
+let multilineBuffer = [];
+let isMultilineMode = false;
+
+// Initialize REPL
+async function initRepl() {
+ try {
+ const response = await fetch('/repl/init', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' }
+ });
+ const data = await response.json();
+ if (data.sessionId) {
+ replSessionId = data.sessionId;
+ console.log('REPL session initialized:', replSessionId);
+
+ // Display version info
+ appendOutput('Nasal REPL interpreter version ' + data.version);
+ appendOutput('Type your code below and press Enter to execute.');
+ appendOutput('Use Shift+Enter for multiline input.\n');
+ showPrompt();
+ } else {
+ throw new Error('Failed to initialize REPL session');
+ }
+ } catch (err) {
+ appendOutput(`Error: ${err.message}`, 'error-line');
+ }
+}
+
+// Format error messages to match command-line REPL
+function formatError(error) {
+ // Split the error message into lines
+ const lines = error.split('\n');
+ return lines.map(line => {
+ // Add appropriate indentation for the error pointer
+ if (line.includes('-->')) {
+ return ' ' + line;
+ } else if (line.includes('^')) {
+ return ' ' + line;
+ }
+ return line;
+ }).join('\n');
+}
+
+// Handle input
+const input = document.getElementById('repl-input');
+input.addEventListener('keydown', async (e) => {
+ if (e.key === 'Enter') {
+ if (e.shiftKey) {
+ // Shift+Enter: add newline
+ const pos = input.selectionStart;
+ const value = input.value;
+ input.value = value.substring(0, pos) + '\n' + value.substring(pos);
+ input.selectionStart = input.selectionEnd = pos + 1;
+ input.style.height = 'auto';
+ input.style.height = input.scrollHeight + 'px';
+ e.preventDefault();
+ return;
+ }
+ e.preventDefault();
+
+ const code = input.value.trim();
+
+ // Skip empty lines but still show prompt
+ if (!code) {
+ showPrompt(isMultilineMode ? '... ' : '>>> ');
+ return;
+ }
+
+ try {
+ // First check if input is complete
+ const checkResponse = await fetch('/repl/eval', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ sessionId: replSessionId,
+ line: code,
+ check: true,
+ buffer: multilineBuffer // Send existing buffer
+ })
+ });
+
+ const checkData = await checkResponse.json();
+
+ if (checkData.needsMore) {
+ // Add to multiline buffer and show continuation prompt
+ multilineBuffer.push(code);
+ isMultilineMode = true;
+
+ // Display the input with continuation prompt
+ appendOutput(code, 'input-line', multilineBuffer.length === 1 ? '>>> ' : '... ');
+
+ input.value = '';
+ showPrompt('... ');
+ return;
+ }
+
+ // If we were in multiline mode, add the final line
+ if (isMultilineMode) {
+ multilineBuffer.push(code);
+ }
+
+ // Get the complete code to evaluate
+ const fullCode = isMultilineMode ?
+ multilineBuffer.join('\n') : code;
+
+ // Display the input
+ appendOutput(code, 'input-line', isMultilineMode ? '... ' : '>>> ');
+
+ // Evaluate the code
+ const response = await fetch('/repl/eval', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ sessionId: replSessionId,
+ line: fullCode
+ })
+ });
+
+ const data = await response.json();
+
+ if (data.error) {
+ appendOutput(formatError(data.error.trim()), 'error-line');
+ } else if (data.result) {
+ handleResult(data.result);
+ }
+
+ // Reset multiline state
+ multilineBuffer = [];
+ isMultilineMode = false;
+ input.value = '';
+ showPrompt('>>> ');
+
+ } catch (err) {
+ appendOutput(`Error: ${err.message}`, 'error-line');
+ multilineBuffer = [];
+ isMultilineMode = false;
+ input.value = '';
+ showPrompt('>>> ');
+ }
+ } else if (e.key === 'ArrowUp') {
+ e.preventDefault();
+ if (historyIndex > 0) {
+ historyIndex--;
+ input.value = commandHistory[historyIndex];
+ }
+ } else if (e.key === 'ArrowDown') {
+ e.preventDefault();
+ if (historyIndex < commandHistory.length - 1) {
+ historyIndex++;
+ input.value = commandHistory[historyIndex];
+ } else {
+ historyIndex = commandHistory.length;
+ input.value = '';
+ }
+ }
+});
+
+// Auto-resize input
+input.addEventListener('input', () => {
+ input.style.height = 'auto';
+ input.style.height = input.scrollHeight + 'px';
+});
+
+// Show prompt and scroll to bottom
+function showPrompt(prompt = '>>> ') {
+ const promptSpan = document.querySelector('.repl-prompt');
+ if (promptSpan) {
+ promptSpan.textContent = prompt;
+ }
+}
+
+// Append output to REPL
+function appendOutput(text, className = '', prefix = '') {
+ const output = document.getElementById('repl-output');
+ const line = document.createElement('div');
+ line.className = `output-line ${className}`;
+
+ line.innerHTML = prefix + formatErrorMessage(text);
+
+
+ output.appendChild(line);
+ output.scrollTop = output.scrollHeight;
+}
+
+// Clear REPL
+function clearREPL() {
+ const output = document.getElementById('repl-output');
+ output.innerHTML = '';
+ appendOutput('Screen cleared', 'system-message');
+ showPrompt();
+}
+
+// Example snippets
+const examples = {
+ basic: `var x = 1011 + 1013;
+println("x = ", x);`,
+
+ loops: `var sum = 0;
+for(var i = 1; i <= 5; i += 1) {
+ sum += i;
+}
+println("Sum:", sum);`,
+
+ functions: `var factorial = func(n) {
+ if (n <= 1) return 1;
+ return n * factorial(n - 1);
+};
+factorial(5);`
+};
+
+// Insert example into input
+function insertExample(type) {
+ input.value = examples[type];
+ input.style.height = 'auto';
+ input.style.height = input.scrollHeight + 'px';
+ input.focus();
+}
+
+// Initialize REPL on page load
+window.addEventListener('load', initRepl);
+
+// Add these utility functions
+function formatErrorMessage(text) {
+ // Replace ANSI escape codes with CSS classes
+ return text
+ // Remove any existing formatting first
+ .replace(/\u001b\[\d+(?:;\d+)*m/g, '')
+ // Format the error line
+ .replace(/^parse: (.+)$/m, 'parse: $1')
+ // Format the file location
+ .replace(/^\s*--> (.+?)(\d+):(\d+)$/m, '--> $1$2:$3')
+ // Format the code line
+ .replace(/^(\d+ \|)(.*)$/m, '$1$2')
+ // Format the error pointer
+ .replace(/^(\s*\|)(\s*)(\^+)$/m, '$1$2$3');
+}
+
+function handleResult(result) {
+ const lines = result.split('\n');
+ lines.forEach(line => {
+ if (line.trim()) {
+ appendOutput(line.trim(), 'result-line');
+ }
+ });
+}
\ No newline at end of file
diff --git a/nasal-web-app/server_repl.js b/nasal-web-app/server_repl.js
new file mode 100644
index 0000000..76153a3
--- /dev/null
+++ b/nasal-web-app/server_repl.js
@@ -0,0 +1,176 @@
+const express = require('express');
+const ffi = require('ffi-napi');
+const path = require('path');
+const yargs = require('yargs/yargs');
+const { hideBin } = require('yargs/helpers');
+
+// Parse command line arguments
+const argv = yargs(hideBin(process.argv))
+ .usage('Usage: $0 [options]')
+ .option('verbose', {
+ alias: 'v',
+ type: 'boolean',
+ description: 'Run with verbose logging'
+ })
+ .option('port', {
+ alias: 'p',
+ type: 'number',
+ description: 'Port to run the server on',
+ default: 3001
+ })
+ .option('host', {
+ type: 'string',
+ description: 'Host to run the server on',
+ default: 'localhost'
+ })
+ .help()
+ .alias('help', 'h')
+ .version()
+ .argv;
+
+const app = express();
+app.use(express.json());
+app.use(express.static('public'));
+
+
+// Load Nasal REPL library functions
+const nasalLib = ffi.Library(path.join(__dirname, '../module/libnasal-web'), {
+ 'nasal_repl_init': ['pointer', []],
+ 'nasal_repl_cleanup': ['void', ['pointer']],
+ 'nasal_repl_eval': ['string', ['pointer', 'string']],
+ 'nasal_repl_is_complete': ['int', ['pointer', 'string']],
+ 'nasal_repl_get_version': ['string', []],
+});
+
+// Store active REPL sessions
+const replSessions = new Map();
+
+// Clean up inactive sessions periodically (30 minutes timeout)
+const SESSION_TIMEOUT = 30 * 60 * 1000;
+setInterval(() => {
+ const now = Date.now();
+ for (const [sessionId, session] of replSessions.entries()) {
+ if (now - session.lastAccess > SESSION_TIMEOUT) {
+ if (argv.verbose) {
+ console.log(`Cleaning up inactive session: ${sessionId}`);
+ }
+ nasalLib.nasal_repl_cleanup(session.context);
+ replSessions.delete(sessionId);
+ }
+ }
+}, 60000); // Check every minute
+
+app.post('/repl/init', (req, res) => {
+ try {
+ const ctx = nasalLib.nasal_repl_init();
+ const sessionId = `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
+ const version = nasalLib.nasal_repl_get_version();
+
+ replSessions.set(sessionId, {
+ context: ctx,
+ lastAccess: Date.now()
+ });
+
+ if (argv.verbose) {
+ console.log(`New REPL session initialized: ${sessionId}`);
+ }
+
+ res.json({
+ sessionId,
+ version
+ });
+ } catch (err) {
+ if (argv.verbose) {
+ console.error('Failed to initialize REPL session:', err);
+ }
+ res.status(500).json({ error: 'Failed to initialize REPL session' });
+ }
+});
+
+app.post('/repl/eval', (req, res) => {
+ const { sessionId, line, check, buffer } = req.body;
+
+ if (!sessionId || !replSessions.has(sessionId)) {
+ return res.status(400).json({ error: 'Invalid or expired session' });
+ }
+
+ if (!line) {
+ return res.status(400).json({ error: 'No code provided' });
+ }
+
+ try {
+ const session = replSessions.get(sessionId);
+ session.lastAccess = Date.now();
+
+ if (check) {
+ const codeToCheck = buffer ? [...buffer, line].join('\n') : line;
+ const isComplete = nasalLib.nasal_repl_is_complete(session.context, codeToCheck);
+
+ if (isComplete === 1) {
+ return res.json({ needsMore: true });
+ } else if (isComplete === -1) {
+ return res.json({ error: 'Invalid input' });
+ }
+ }
+
+ const result = nasalLib.nasal_repl_eval(session.context, line);
+
+ if (argv.verbose) {
+ console.log(`REPL evaluation for session ${sessionId}:`, { line, result });
+ }
+
+ res.json({ result });
+ } catch (err) {
+ if (argv.verbose) {
+ console.error(`REPL evaluation error for session ${sessionId}:`, err);
+ }
+ res.status(500).json({ error: err.message });
+ }
+});
+
+app.post('/repl/cleanup', (req, res) => {
+ const { sessionId } = req.body;
+
+ if (!sessionId || !replSessions.has(sessionId)) {
+ return res.status(400).json({ error: 'Invalid session' });
+ }
+
+ try {
+ const session = replSessions.get(sessionId);
+ nasalLib.nasal_repl_cleanup(session.context);
+ replSessions.delete(sessionId);
+
+ if (argv.verbose) {
+ console.log(`REPL session cleaned up: ${sessionId}`);
+ }
+
+ res.json({ message: 'Session cleaned up successfully' });
+ } catch (err) {
+ if (argv.verbose) {
+ console.error(`Failed to cleanup session ${sessionId}:`, err);
+ }
+ res.status(500).json({ error: err.message });
+ }
+});
+
+// Handle cleanup on server shutdown
+process.on('SIGINT', () => {
+ console.log('\nCleaning up REPL sessions before exit...');
+ for (const [sessionId, session] of replSessions.entries()) {
+ try {
+ nasalLib.nasal_repl_cleanup(session.context);
+ if (argv.verbose) {
+ console.log(`Cleaned up session: ${sessionId}`);
+ }
+ } catch (err) {
+ console.error(`Error cleaning up session ${sessionId}:`, err);
+ }
+ }
+ process.exit(0);
+});
+
+const PORT = argv.port || 3001;
+app.listen(PORT, () => {
+ console.log(`REPL Server running on http://localhost:${PORT}`);
+ if (argv.verbose) console.log('Verbose logging enabled');
+});
\ No newline at end of file