commit
0e85bfccab
|
@ -1,3 +1,21 @@
|
|||
# Build directories
|
||||
build/
|
||||
out/
|
||||
dist/
|
||||
cmake-build-*/
|
||||
|
||||
# IDE and editor files
|
||||
.vscode/
|
||||
.idea/
|
||||
.vs/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
.DS_Store
|
||||
.env
|
||||
.env.local
|
||||
|
||||
# C++ specific
|
||||
# Prerequisites
|
||||
*.d
|
||||
|
||||
|
@ -30,32 +48,58 @@
|
|||
*.exe
|
||||
*.out
|
||||
*.app
|
||||
nasal
|
||||
nasal.exe
|
||||
|
||||
# VS C++ sln
|
||||
# Visual Studio specific
|
||||
*.sln
|
||||
*.vcxproj
|
||||
*.vcxproj.filters
|
||||
*.vcxproj.user
|
||||
.vs
|
||||
x64
|
||||
x64/
|
||||
CMakePresents.json
|
||||
ipch/
|
||||
*.aps
|
||||
*.ncb
|
||||
*.opendb
|
||||
*.opensdf
|
||||
*.sdf
|
||||
*.cachefile
|
||||
*.VC.db
|
||||
*.VC.VC.opendb
|
||||
|
||||
# nasal executable
|
||||
nasal
|
||||
nasal.exe
|
||||
# CMake
|
||||
CMakeCache.txt
|
||||
CMakeFiles/
|
||||
cmake_install.cmake
|
||||
install_manifest.txt
|
||||
CTestTestfile.cmake
|
||||
_deps/
|
||||
|
||||
# misc
|
||||
.vscode
|
||||
dump
|
||||
# Node.js specific (for the web app)
|
||||
node_modules/
|
||||
npm-debug.log
|
||||
yarn-debug.log
|
||||
yarn-error.log
|
||||
package-lock.json
|
||||
|
||||
# Project specific
|
||||
dump/
|
||||
fgfs.log
|
||||
.temp.*
|
||||
*.ppm
|
||||
|
||||
# build dir
|
||||
build
|
||||
out
|
||||
# Logs and databases
|
||||
*.log
|
||||
*.sqlite
|
||||
*.sqlite3
|
||||
*.db
|
||||
|
||||
# macOS special cache directory
|
||||
# OS generated files
|
||||
.DS_Store
|
||||
|
||||
# ppm picture generated by ppmgen.nas
|
||||
*.ppm
|
||||
.DS_Store?
|
||||
._*
|
||||
.Spotlight-V100
|
||||
.Trashes
|
||||
ehthumbs.db
|
||||
Thumbs.db
|
|
@ -100,3 +100,15 @@ target_link_libraries(mat module-used-object)
|
|||
add_library(nasock SHARED ${CMAKE_SOURCE_DIR}/module/nasocket.cpp)
|
||||
target_include_directories(nasock PRIVATE ${CMAKE_SOURCE_DIR}/src)
|
||||
target_link_libraries(nasock module-used-object)
|
||||
|
||||
# Add web library
|
||||
add_library(nasal-web SHARED
|
||||
src/nasal_web.cpp
|
||||
${NASAL_OBJECT_SOURCE_FILE}
|
||||
)
|
||||
target_include_directories(nasal-web PRIVATE ${CMAKE_SOURCE_DIR}/src)
|
||||
set_target_properties(nasal-web PROPERTIES
|
||||
C_VISIBILITY_PRESET hidden
|
||||
CXX_VISIBILITY_PRESET hidden
|
||||
VISIBILITY_INLINES_HIDDEN ON
|
||||
)
|
||||
|
|
55
README.md
55
README.md
|
@ -23,6 +23,7 @@
|
|||
* [__Trace Back Info__](#trace-back-info)
|
||||
* [__Debugger__](#debugger)
|
||||
* [__REPL__](#repl)
|
||||
* [__Web Interface__](#web-interface)
|
||||
|
||||
__Contact us if having great ideas to share!__
|
||||
|
||||
|
@ -121,7 +122,7 @@ runtime.windows.set_utf8_output();
|
|||
|
||||
![error](./doc/gif/error.gif)
|
||||
|
||||
<details><summary>Must use `var` to define variables</summary>
|
||||
<details><summary>Must use `var` to define variables</summary>
|
||||
|
||||
This interpreter uses more strict syntax to make sure it is easier for you to program and debug.
|
||||
And flightgear's nasal interpreter also has the same rule.
|
||||
|
@ -146,13 +147,13 @@ If you forget to add the keyword `var`, you will get this:
|
|||
```javascript
|
||||
code: undefined symbol "i"
|
||||
--> test.nas:1:9
|
||||
|
|
||||
|
|
||||
1 | foreach(i; [0, 1, 2, 3])
|
||||
| ^ undefined symbol "i"
|
||||
|
||||
code: undefined symbol "i"
|
||||
--> test.nas:2:11
|
||||
|
|
||||
|
|
||||
2 | print(i)
|
||||
| ^ undefined symbol "i"
|
||||
```
|
||||
|
@ -441,5 +442,51 @@ Nasal REPL interpreter version 11.1 (Nov 1 2023 23:37:30)
|
|||
>>> use std.json;
|
||||
{stringify:func(..) {..},parse:func(..) {..}}
|
||||
|
||||
>>>
|
||||
>>>
|
||||
```
|
||||
|
||||
## __Web Interface__
|
||||
|
||||
A web-based interface is now available for trying out Nasal code directly in your browser. It includes both a code editor and an interactive REPL (WIP).
|
||||
|
||||
### Web Code Editor
|
||||
- Syntax highlighting using CodeMirror
|
||||
- Error highlighting and formatting
|
||||
- Example programs
|
||||
- Execution time display option
|
||||
- Configurable execution time limits
|
||||
- Notice: The security of the online interpreter is not well tested, please use it with sandbox mechanism or other security measures.
|
||||
|
||||
### Web REPL
|
||||
- ** IMPORTANT: The time limit in REPL is not correctly implemented yet. Thus this REPL web binding is not considered finished. Do not use it in production before it's fixed. **
|
||||
- Interactive command-line style interface in browser
|
||||
- Multi-line input support with proper prompts (`>>>` and `...`)
|
||||
- Command history navigation
|
||||
- Error handling with formatted error messages
|
||||
- Example snippets for quick testing
|
||||
|
||||
### Running the Web Interface
|
||||
|
||||
1. Build the Nasal shared library:
|
||||
```bash
|
||||
cmake -DBUILD_SHARED_LIBS=ON .
|
||||
make nasal-web
|
||||
```
|
||||
|
||||
2. Set up and run the web application:
|
||||
|
||||
For the code editor:
|
||||
```bash
|
||||
cd nasal-web-app
|
||||
npm install
|
||||
node server.js
|
||||
```
|
||||
Visit `http://127.0.0.1:3000/`
|
||||
|
||||
For the REPL:
|
||||
```bash
|
||||
cd nasal-web-app
|
||||
npm install
|
||||
node server_repl.js
|
||||
```
|
||||
Visit `http://127.0.0.1:3001/repl.html`
|
||||
|
|
|
@ -26,9 +26,8 @@
|
|||
|
||||
__如果有好的意见或建议,欢迎联系我们!__
|
||||
|
||||
* __lhk101lhk101@qq.com__ (ValKmjolnir)
|
||||
|
||||
* __sidi.liang@gmail.com__ (Sidi)
|
||||
- __lhk101lhk101@qq.com__ (ValKmjolnir)
|
||||
- __sidi.liang@gmail.com__ (Sidi)
|
||||
|
||||
## __简介__
|
||||
|
||||
|
@ -428,3 +427,57 @@ Nasal REPL interpreter version 11.1 (Nov 1 2023 23:37:30)
|
|||
|
||||
>>>
|
||||
```
|
||||
## __Web 界面__
|
||||
|
||||
现已提供基于 Web 的库以及示例界面,您可以直接在浏览器中编写和运行 Nasal 代码。该界面包括代码编辑器和交互式 REPL(未完成)。
|
||||
|
||||
### __Web 代码编辑器__
|
||||
|
||||
- **语法高亮:** 使用 CodeMirror 提供增强的编码体验。
|
||||
- **错误高亮和格式化:** 清晰显示语法和运行时错误。
|
||||
- **示例程序:** 预加载的示例,帮助您快速上手。
|
||||
- **执行时间显示选项:** 可选择查看代码执行所需时间。
|
||||
- **可配置的执行时间限制:** 设置时间限制以防止代码长时间运行。
|
||||
- **提示:** 在线解释器的安全性尚未得到广泛测试,建议配合沙盒机制等安全措施使用。
|
||||
|
||||
### __Web REPL__
|
||||
|
||||
- **重要提示:** REPL 中的代码执行时间限制尚未正确实现。此 REPL 库目前不稳定,请勿在生产环境中使用。
|
||||
- **交互式命令行界面:** 在浏览器中体验熟悉的 REPL 环境。
|
||||
- **多行输入支持:** 使用 `>>>` 和 `...` 提示符无缝输入多行代码。
|
||||
- **命令历史导航:** 使用箭头键轻松浏览命令历史。
|
||||
- **格式化的错误处理:** 接收清晰且格式化的错误消息,助力调试。
|
||||
- **快速测试的示例代码片段:** 访问并运行示例代码片段,快速测试功能。
|
||||
|
||||
### __运行 Web 界面__
|
||||
|
||||
1. **构建 Nasal 共享库:**
|
||||
|
||||
```bash
|
||||
cmake -DBUILD_SHARED_LIBS=ON .
|
||||
make nasal-web
|
||||
```
|
||||
|
||||
2. **设置并运行 Web 应用:**
|
||||
|
||||
**代码编辑器:**
|
||||
|
||||
```bash
|
||||
cd nasal-web-app
|
||||
npm install
|
||||
node server.js
|
||||
```
|
||||
|
||||
在浏览器中访问 `http://127.0.0.1:3000/` 以使用代码编辑器。
|
||||
|
||||
**REPL:**
|
||||
|
||||
```bash
|
||||
cd nasal-web-app
|
||||
npm install
|
||||
node server_repl.js
|
||||
```
|
||||
|
||||
在浏览器中访问 `http://127.0.0.1:3001/repl.html` 以使用 REPL 界面。
|
||||
|
||||
---
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"name": "nasal-web-app",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"express": "^4.21.1",
|
||||
"ffi-napi": "^4.0.3",
|
||||
"yargs": "^17.7.2"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,638 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Nasal Interpreter Web Demo</title>
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/codemirror.min.css" rel="stylesheet">
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/theme/monokai.min.css" rel="stylesheet">
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.header {
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
padding: 30px;
|
||||
background: linear-gradient(145deg, #2c3e50, #3498db);
|
||||
border-radius: 12px;
|
||||
color: white;
|
||||
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.logo {
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.ascii-art {
|
||||
font-family: 'Monaco', monospace;
|
||||
color: #ecf0f1;
|
||||
text-shadow: 0 0 10px rgba(255,255,255,0.3);
|
||||
margin: 0;
|
||||
line-height: 1.2;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 2.5em;
|
||||
margin: 0 0 10px 0;
|
||||
font-weight: 700;
|
||||
background: linear-gradient(to right, #fff, #ecf0f1);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
text-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 1.2em;
|
||||
color: #ecf0f1;
|
||||
margin: 0 0 25px 0;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.credits {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 30px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.credit-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.credit-label {
|
||||
color: #bdc3c7;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.credit-link {
|
||||
text-decoration: none;
|
||||
padding: 5px 10px;
|
||||
border-radius: 20px;
|
||||
background: rgba(255,255,255,0.1);
|
||||
transition: all 0.3s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.credit-link:hover {
|
||||
background: rgba(255,255,255,0.2);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
.highlight {
|
||||
color: #fff;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.author {
|
||||
color: #bdc3c7;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.header {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 2em;
|
||||
}
|
||||
|
||||
.credits {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
.editor-container {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.code-section, .output-section {
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.CodeMirror {
|
||||
height: 400px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.output-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
display: inline-block;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.status-success {
|
||||
background-color: #2ecc71;
|
||||
}
|
||||
|
||||
.status-error {
|
||||
background-color: #e74c3c;
|
||||
}
|
||||
|
||||
#output {
|
||||
height: 400px;
|
||||
background: #282c34;
|
||||
color: #abb2bf;
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
border: none;
|
||||
overflow-y: auto;
|
||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.message {
|
||||
margin: 4px 0;
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
background: #2c313a;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
border-left: 3px solid #e06c75;
|
||||
}
|
||||
|
||||
.success-message {
|
||||
border-left: 3px solid #98c379;
|
||||
}
|
||||
|
||||
.timestamp {
|
||||
color: #5c6370;
|
||||
font-size: 0.9em;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.terminal-output {
|
||||
margin: 8px 0 0 0;
|
||||
padding: 8px;
|
||||
background: #21252b;
|
||||
border-radius: 3px;
|
||||
white-space: pre-wrap;
|
||||
overflow-x: auto;
|
||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.terminal-output:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.message + .message {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
display: inline-block;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
.status-success {
|
||||
background-color: #98c379;
|
||||
}
|
||||
|
||||
.status-error {
|
||||
background-color: #e06c75;
|
||||
}
|
||||
|
||||
.output-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.output-header h2 {
|
||||
margin: 0;
|
||||
color: #abb2bf;
|
||||
}
|
||||
|
||||
.controls {
|
||||
text-align: center;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
button {
|
||||
background-color: #3498db;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
transition: background-color 0.3s;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-color: #2980b9;
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
background-color: #bdc3c7;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.examples {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.example-btn {
|
||||
background-color: #27ae60;
|
||||
margin: 0 5px;
|
||||
}
|
||||
|
||||
.example-btn:hover {
|
||||
background-color: #219a52;
|
||||
}
|
||||
|
||||
.terminal-output {
|
||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||
margin: 0;
|
||||
padding: 8px;
|
||||
background: #2c3e50;
|
||||
color: #ecf0f1;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.timestamp {
|
||||
font-size: 0.9em;
|
||||
color: #7f8c8d;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.terminal-red-bold {
|
||||
color: #ff5555;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.terminal-cyan-bold {
|
||||
color: #8be9fd;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.terminal-bold {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.terminal-caret {
|
||||
color: #ff5555;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
background: #2c3e50;
|
||||
border-left: 4px solid #e74c3c;
|
||||
padding: 10px;
|
||||
margin: 5px 0;
|
||||
color: #ecf0f1;
|
||||
}
|
||||
|
||||
.success-message {
|
||||
background: #2c3e50;
|
||||
border-left: 4px solid #2ecc71;
|
||||
padding: 10px;
|
||||
margin: 5px 0;
|
||||
color: #ecf0f1;
|
||||
}
|
||||
|
||||
#output {
|
||||
background: #34495e;
|
||||
color: #ecf0f1;
|
||||
padding: 15px;
|
||||
border-radius: 4px;
|
||||
border: none;
|
||||
box-shadow: inset 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.error-message pre, .success-message pre {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Error message styling */
|
||||
.error-type {
|
||||
color: #e06c75;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.error-desc {
|
||||
color: #abb2bf;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.error-arrow {
|
||||
color: #56b6c2;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.error-file {
|
||||
color: #e06c75;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.error-loc {
|
||||
color: #abb2bf;
|
||||
}
|
||||
|
||||
.error-line-number {
|
||||
color: #56b6c2;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.error-code {
|
||||
color: #abb2bf;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.error-pointer-space {
|
||||
color: #abb2bf;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.error-pointer {
|
||||
color: #e06c75;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.terminal-output {
|
||||
margin: 8px 0 0 0;
|
||||
padding: 8px;
|
||||
background: #21252b;
|
||||
border-radius: 3px;
|
||||
white-space: pre-wrap;
|
||||
overflow-x: auto;
|
||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.timing-checkbox {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
margin-left: 10px;
|
||||
color: #abb2bf;
|
||||
}
|
||||
|
||||
.timing-checkbox input {
|
||||
margin-right: 5px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<div class="logo">
|
||||
<pre class="ascii-art">
|
||||
__ _
|
||||
/\ \ \__ _ ___ __ _| |
|
||||
/ \/ / _` / __|/ _` | |
|
||||
/ /\ / (_| \__ \ (_| | |
|
||||
\_\ \/ \__,_|___/\__,_|_|
|
||||
</pre>
|
||||
</div>
|
||||
<h1>Nasal Interpreter Web Demo</h1>
|
||||
<p class="subtitle">Write and execute Nasal code directly in your browser</p>
|
||||
<div class="credits">
|
||||
<div class="credit-item">
|
||||
<span class="credit-label">Powered by</span>
|
||||
<a href="https://www.fgprc.org.cn/nasal_interpreter.html" class="credit-link">
|
||||
<span class="highlight">Nasal Interpreter</span>
|
||||
<span class="author">by ValKmjolnir</span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="credit-item">
|
||||
<span class="credit-label">Web App by</span>
|
||||
<a href="https://sidi762.github.io" class="credit-link">
|
||||
<span class="highlight">LIANG Sidi</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="editor-container">
|
||||
<div class="code-section">
|
||||
<h2>Code Editor</h2>
|
||||
<textarea id="code">var x = 1 + 2;
|
||||
println(x);</textarea>
|
||||
</div>
|
||||
<div class="output-section">
|
||||
<div class="output-header">
|
||||
<h2>Output</h2>
|
||||
<div id="status"></div>
|
||||
</div>
|
||||
<div id="output"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="controls">
|
||||
<button id="runBtn" onclick="runCode()">Run</button>
|
||||
<button onclick="clearOutput()">Clear Output</button>
|
||||
<label class="timing-checkbox">
|
||||
<input type="checkbox" id="showTime"> Show execution time
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="examples">
|
||||
<h3>Example Programs:</h3>
|
||||
<button class="example-btn" onclick="loadExample('basic')">Basic Math</button>
|
||||
<button class="example-btn" onclick="loadExample('loops')">Loops</button>
|
||||
<button class="example-btn" onclick="loadExample('functions')">Functions</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/codemirror.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/mode/javascript/javascript.min.js"></script>
|
||||
<script>
|
||||
// Initialize CodeMirror
|
||||
var editor = CodeMirror.fromTextArea(document.getElementById("code"), {
|
||||
lineNumbers: true,
|
||||
mode: "javascript", // Using JavaScript mode as it's close enough to Nasal
|
||||
theme: "monokai",
|
||||
autoCloseBrackets: true,
|
||||
matchBrackets: true,
|
||||
indentUnit: 4,
|
||||
tabSize: 4,
|
||||
lineWrapping: true
|
||||
});
|
||||
|
||||
// Example programs
|
||||
const examples = {
|
||||
basic: `# Basic math operations
|
||||
var a = 10;
|
||||
var b = 5;
|
||||
|
||||
println("Addition: ", a + b);
|
||||
println("Subtraction: ", a - b);
|
||||
println("Multiplication: ", a * b);
|
||||
println("Division: ", a / b);`,
|
||||
|
||||
loops: `# Loop example
|
||||
var sum = 0;
|
||||
for(var i = 1; i <= 5; i += 1) {
|
||||
sum += i;
|
||||
println("Current sum: ", sum);
|
||||
}
|
||||
println("Final sum: ", sum);`,
|
||||
|
||||
functions: `# Function example
|
||||
var factorial = func(n) {
|
||||
if (n <= 1) return 1;
|
||||
return n * factorial(n - 1);
|
||||
}
|
||||
|
||||
for(var i = 0; i <= 5; i += 1) {
|
||||
println("Factorial of ", i, " is ", factorial(i));
|
||||
}`
|
||||
};
|
||||
|
||||
function loadExample(type) {
|
||||
editor.setValue(examples[type]);
|
||||
}
|
||||
|
||||
function formatTerminalOutput(text) {
|
||||
// Remove the output/error message header if present
|
||||
text = text.replace(/^\[(.*?)\] (Output|Error):\s*\n/, '');
|
||||
|
||||
// Convert ANSI color codes to CSS classes
|
||||
const colorMap = {
|
||||
'\u001b[91;1m': '<span class="terminal-red-bold">', // bright red bold
|
||||
'\u001b[36;1m': '<span class="terminal-cyan-bold">', // bright cyan bold
|
||||
'\u001b[0m': '</span>', // reset
|
||||
'\u001b[1m': '<span class="terminal-bold">' // bold
|
||||
};
|
||||
|
||||
// Replace ANSI codes with HTML spans
|
||||
Object.entries(colorMap).forEach(([code, html]) => {
|
||||
text = text.replace(new RegExp(escapeRegExp(code), 'g'), html);
|
||||
});
|
||||
|
||||
// Preserve whitespace and arrow formatting
|
||||
text = text.replace(/\^/g, '<span class="terminal-caret">^</span>');
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
// Utility function to escape special characters in strings for RegExp
|
||||
function escapeRegExp(string) {
|
||||
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
}
|
||||
|
||||
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, '<span class="error-type">parse:</span> <span class="error-desc">$1</span>')
|
||||
// Format the file location
|
||||
.replace(/^\s*--> (.+?)(\d+):(\d+)$/m, '<span class="error-arrow">--></span> <span class="error-file">$1</span><span class="error-loc">$2:$3</span>')
|
||||
// Format the code line
|
||||
.replace(/^(\d+ \|)(.*)$/m, '<span class="error-line-number">$1</span><span class="error-code">$2</span>')
|
||||
// Format the error pointer
|
||||
.replace(/^(\s*\|)(\s*)(\^+)$/m, '<span class="error-line-number">$1</span><span class="error-pointer-space">$2</span><span class="error-pointer">$3</span>');
|
||||
}
|
||||
|
||||
async function runCode() {
|
||||
const runBtn = document.getElementById('runBtn');
|
||||
const output = document.getElementById('output');
|
||||
const status = document.getElementById('status');
|
||||
const showTime = document.getElementById('showTime').checked;
|
||||
|
||||
try {
|
||||
runBtn.disabled = true;
|
||||
const code = editor.getValue();
|
||||
|
||||
const response = await fetch('/eval', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
code,
|
||||
showTime
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
const timestamp = new Date().toLocaleTimeString();
|
||||
|
||||
if (data.error) {
|
||||
status.innerHTML = '<span class="status-indicator status-error"></span>';
|
||||
output.innerHTML += `<div class="message error-message">
|
||||
<span class="timestamp">[${timestamp}]</span>
|
||||
<pre class="terminal-output">${formatErrorMessage(data.error)}</pre>
|
||||
</div>`;
|
||||
} else {
|
||||
status.innerHTML = '<span class="status-indicator status-success"></span>';
|
||||
output.innerHTML += `<div class="message success-message">
|
||||
<span class="timestamp">[${timestamp}]</span>
|
||||
<pre class="terminal-output">${data.result.trim()}</pre>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
output.scrollTop = output.scrollHeight;
|
||||
} catch (err) {
|
||||
status.innerHTML = '<span class="status-indicator status-error"></span>';
|
||||
output.innerHTML += `<div class="message error-message">
|
||||
<span class="timestamp">[${new Date().toLocaleTimeString()}]</span>
|
||||
<pre class="terminal-output">Server Error: ${err.message}</pre>
|
||||
</div>`;
|
||||
} finally {
|
||||
runBtn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
function clearOutput() {
|
||||
document.getElementById('output').innerHTML = '';
|
||||
document.getElementById('status').innerHTML = '';
|
||||
}
|
||||
|
||||
// Utility function to escape HTML to prevent XSS
|
||||
function escapeHtml(text) {
|
||||
const map = {
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
"'": '''
|
||||
};
|
||||
return text.replace(/[&<>"']/g, function(m) { return map[m]; });
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,240 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Nasal Interpreter Web REPL</title>
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/codemirror.min.css" rel="stylesheet">
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/theme/monokai.min.css" rel="stylesheet">
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
background-color: #2c3e50;
|
||||
color: #ecf0f1;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1000px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.header {
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 2.5em;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.repl-container {
|
||||
background: #34495e;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
height: 600px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.repl-output {
|
||||
flex-grow: 1;
|
||||
background: #21252b;
|
||||
color: #abb2bf;
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 10px;
|
||||
overflow-y: auto;
|
||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||
line-height: 1.5;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.repl-input-container {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.repl-prompt {
|
||||
color: #3498db;
|
||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||
padding: 5px;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.repl-input {
|
||||
flex-grow: 1;
|
||||
background: #21252b;
|
||||
border: none;
|
||||
color: #abb2bf;
|
||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||
font-size: 14px;
|
||||
padding: 5px;
|
||||
outline: none;
|
||||
resize: none;
|
||||
min-height: 24px;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
.controls {
|
||||
text-align: center;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
button {
|
||||
background-color: #3498db;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
transition: background-color 0.3s;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-color: #2980b9;
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
background-color: #bdc3c7;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.examples {
|
||||
margin-top: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.example-btn {
|
||||
background-color: #27ae60;
|
||||
margin: 0 5px;
|
||||
}
|
||||
|
||||
.example-btn:hover {
|
||||
background-color: #219a52;
|
||||
}
|
||||
|
||||
.output-line {
|
||||
margin: 2px 0;
|
||||
min-height: 1.2em;
|
||||
}
|
||||
|
||||
.input-line {
|
||||
color: #3498db;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.error-line {
|
||||
color: #e74c3c;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.result-line {
|
||||
color: #2ecc71;
|
||||
white-space: pre;
|
||||
margin-left: 4px; /* Slight indent for results */
|
||||
}
|
||||
|
||||
.system-message {
|
||||
color: #7f8c8d;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.help-text {
|
||||
color: #95a5a6;
|
||||
white-space: pre;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.error-type {
|
||||
color: #ff5f5f;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.error-desc {
|
||||
color: #abb2bf;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.error-arrow, .error-line-number {
|
||||
color: #56b6c2;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.error-file {
|
||||
color: #ff5f5f;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.error-code {
|
||||
color: #abb2bf;
|
||||
}
|
||||
|
||||
.error-pointer-space {
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.error-pointer {
|
||||
color: #ff5f5f;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.error-red-bold {
|
||||
color: #e06c75;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.error-cyan-bold {
|
||||
color: #56b6c2;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.error-bold {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.repl-container {
|
||||
height: 400px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>Nasal Interpreter Web REPL</h1>
|
||||
<p>Interactive Read-Eval-Print Loop for Nasal</p>
|
||||
<p>Powered by ValKmjolnir's <a href="https://www.fgprc.org.cn/nasal_interpreter.html">Nasal Interpreter</a>, Web App by <a href="https://sidi762.github.io">LIANG Sidi</a></p>
|
||||
</div>
|
||||
|
||||
<div class="repl-container">
|
||||
<div id="repl-output" class="repl-output">
|
||||
<div class="output-line">Welcome to Nasal Web REPL Demo!</div>
|
||||
</div>
|
||||
<div class="repl-input-container">
|
||||
<span class="repl-prompt">>>></span>
|
||||
<textarea id="repl-input" class="repl-input" rows="1"
|
||||
placeholder="Enter Nasal code here..."></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="controls">
|
||||
<button onclick="clearREPL()">Clear REPL</button>
|
||||
</div>
|
||||
|
||||
<div class="examples">
|
||||
<h3>Example Commands:</h3>
|
||||
<button class="example-btn" onclick="insertExample('basic')">Basic Math</button>
|
||||
<button class="example-btn" onclick="insertExample('loops')">Loops</button>
|
||||
<button class="example-btn" onclick="insertExample('functions')">Functions</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="repl.js"></script>
|
||||
</body>
|
||||
</html>
|
|
@ -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, '<span class="error-type">parse:</span> <span class="error-desc">$1</span>')
|
||||
// Format the file location
|
||||
.replace(/^\s*--> (.+?)(\d+):(\d+)$/m, '<span class="error-arrow">--></span> <span class="error-file">$1</span><span class="error-loc">$2:$3</span>')
|
||||
// Format the code line
|
||||
.replace(/^(\d+ \|)(.*)$/m, '<span class="error-line-number">$1</span><span class="error-code">$2</span>')
|
||||
// Format the error pointer
|
||||
.replace(/^(\s*\|)(\s*)(\^+)$/m, '<span class="error-line-number">$1</span><span class="error-pointer-space">$2</span><span class="error-pointer">$3</span>');
|
||||
}
|
||||
|
||||
function handleResult(result) {
|
||||
const lines = result.split('\n');
|
||||
lines.forEach(line => {
|
||||
if (line.trim()) {
|
||||
appendOutput(line.trim(), 'result-line');
|
||||
}
|
||||
});
|
||||
}
|
|
@ -0,0 +1,94 @@
|
|||
const express = require('express');
|
||||
const path = require('path');
|
||||
const yargs = require('yargs/yargs');
|
||||
const { hideBin } = require('yargs/helpers');
|
||||
const koffi = require('koffi');
|
||||
|
||||
// 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: 3000
|
||||
})
|
||||
.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'));
|
||||
|
||||
let nasalLib;
|
||||
try {
|
||||
// First load the library
|
||||
const lib = koffi.load(path.join(__dirname, '../module/libnasal-web.dylib'));
|
||||
|
||||
// Then declare the functions explicitly
|
||||
nasalLib = {
|
||||
nasal_init: lib.func('nasal_init', 'void*', []),
|
||||
nasal_cleanup: lib.func('nasal_cleanup', 'void', ['void*']),
|
||||
nasal_eval: lib.func('nasal_eval', 'const char*', ['void*', 'const char*', 'int']),
|
||||
nasal_get_error: lib.func('nasal_get_error', 'const char*', ['void*'])
|
||||
};
|
||||
|
||||
} catch (err) {
|
||||
console.error('Failed to load nasal library:', err);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
app.post('/eval', (req, res) => {
|
||||
const { code, showTime = false } = req.body;
|
||||
if (!code) {
|
||||
return res.status(400).json({ error: 'No code provided' });
|
||||
}
|
||||
|
||||
if (argv.verbose) {
|
||||
console.log('Received code evaluation request:', code);
|
||||
console.log('Show time:', showTime);
|
||||
}
|
||||
|
||||
const ctx = nasalLib.nasal_init();
|
||||
try {
|
||||
const result = nasalLib.nasal_eval(ctx, code, showTime ? 1 : 0);
|
||||
const error = nasalLib.nasal_get_error(ctx);
|
||||
|
||||
if (error && error !== 'null') {
|
||||
if (argv.verbose) console.log('Nasal error:', error);
|
||||
res.json({ error: error });
|
||||
} else if (result && result.trim() !== '') {
|
||||
if (argv.verbose) console.log('Nasal output:', result);
|
||||
res.json({ result: result });
|
||||
} else {
|
||||
if (argv.verbose) console.log('No output or error returned');
|
||||
res.json({ error: 'No output or error returned' });
|
||||
}
|
||||
} catch (err) {
|
||||
if (argv.verbose) console.error('Server error:', err);
|
||||
res.status(500).json({ error: err.message });
|
||||
} finally {
|
||||
if (argv.verbose) console.log('Cleaning up Nasal context');
|
||||
nasalLib.nasal_cleanup(ctx);
|
||||
}
|
||||
});
|
||||
|
||||
const PORT = argv.port || 3000;
|
||||
app.listen(PORT, () => {
|
||||
console.log(`Server running on port ${PORT}`);
|
||||
console.log(`Visit http://localhost:${PORT} to use the Nasal interpreter`);
|
||||
if (argv.verbose) console.log('Verbose logging enabled');
|
||||
});
|
|
@ -0,0 +1,188 @@
|
|||
const express = require('express');
|
||||
const path = require('path');
|
||||
const yargs = require('yargs/yargs');
|
||||
const { hideBin } = require('yargs/helpers');
|
||||
const koffi = require('koffi');
|
||||
|
||||
// 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
|
||||
let nasalLib;
|
||||
try {
|
||||
const lib = koffi.load(path.join(__dirname, '../module/libnasal-web.dylib'));
|
||||
|
||||
nasalLib = {
|
||||
nasal_repl_init: lib.func('nasal_repl_init', 'void*', []),
|
||||
nasal_repl_cleanup: lib.func('nasal_repl_cleanup', 'void', ['void*']),
|
||||
nasal_repl_eval: lib.func('nasal_repl_eval', 'const char*', ['void*', 'const char*']),
|
||||
nasal_repl_is_complete: lib.func('nasal_repl_is_complete', 'int', ['void*', 'const char*']),
|
||||
nasal_repl_get_version: lib.func('nasal_repl_get_version', 'const char*', [])
|
||||
};
|
||||
|
||||
if (argv.verbose) {
|
||||
console.log('REPL Library loaded successfully');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to load REPL library:', err);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// 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');
|
||||
});
|
|
@ -0,0 +1 @@
|
|||
../std
|
|
@ -0,0 +1,367 @@
|
|||
#include "nasal_web.h"
|
||||
#include "nasal_vm.h"
|
||||
#include "nasal_parse.h"
|
||||
#include "nasal_codegen.h"
|
||||
#include "nasal_import.h"
|
||||
#include "optimizer.h"
|
||||
#include "nasal_err.h"
|
||||
#include "nasal_lexer.h"
|
||||
#include "repl/repl.h"
|
||||
|
||||
#include <string>
|
||||
#include <sstream>
|
||||
#include <fstream>
|
||||
#include <cstdlib>
|
||||
#include <cstdio>
|
||||
#include <stdexcept>
|
||||
#include <chrono>
|
||||
#include <vector>
|
||||
#include <future>
|
||||
|
||||
namespace {
|
||||
// Helper function implementations inside anonymous namespace
|
||||
std::vector<std::string> split_string(const std::string& str, char delim) {
|
||||
std::vector<std::string> result;
|
||||
std::stringstream ss(str);
|
||||
std::string item;
|
||||
while (std::getline(ss, item, delim)) {
|
||||
result.push_back(item);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
std::string join_string(const std::vector<std::string>& vec, const std::string& delim) {
|
||||
if (vec.empty()) return "";
|
||||
std::stringstream ss;
|
||||
ss << vec[0];
|
||||
for (size_t i = 1; i < vec.size(); ++i) {
|
||||
ss << delim << vec[i];
|
||||
}
|
||||
return ss.str();
|
||||
}
|
||||
}
|
||||
|
||||
struct NasalContext {
|
||||
std::unique_ptr<nasal::vm> vm_instance;
|
||||
std::string last_result;
|
||||
std::string last_error;
|
||||
std::chrono::seconds timeout{5}; // Default 5 second timeout
|
||||
|
||||
NasalContext() {
|
||||
vm_instance = std::make_unique<nasal::vm>();
|
||||
}
|
||||
};
|
||||
|
||||
struct WebReplContext {
|
||||
std::unique_ptr<nasal::repl::repl> repl_instance;
|
||||
std::vector<std::string> source;
|
||||
std::string last_result;
|
||||
std::string last_error;
|
||||
bool allow_output;
|
||||
bool initialized;
|
||||
std::chrono::seconds timeout{1}; // Default 1 second timeout
|
||||
|
||||
WebReplContext() : allow_output(false), initialized(false) {
|
||||
repl_instance = std::make_unique<nasal::repl::repl>();
|
||||
}
|
||||
};
|
||||
|
||||
void* nasal_init() {
|
||||
return new NasalContext();
|
||||
}
|
||||
|
||||
void nasal_cleanup(void* context) {
|
||||
delete static_cast<NasalContext*>(context);
|
||||
}
|
||||
|
||||
// Add new function to set timeout
|
||||
void nasal_set_timeout(void* context, int seconds) {
|
||||
auto* ctx = static_cast<NasalContext*>(context);
|
||||
ctx->timeout = std::chrono::seconds(seconds);
|
||||
}
|
||||
|
||||
const char* nasal_eval(void* context, const char* code, int show_time) {
|
||||
using clk = std::chrono::high_resolution_clock;
|
||||
const auto den = clk::duration::period::den;
|
||||
|
||||
auto* ctx = static_cast<NasalContext*>(context);
|
||||
|
||||
try {
|
||||
nasal::lexer lex;
|
||||
nasal::parse parse;
|
||||
nasal::linker ld;
|
||||
nasal::codegen gen;
|
||||
|
||||
// Create a unique temporary file
|
||||
char temp_filename[256];
|
||||
snprintf(temp_filename, sizeof(temp_filename), "/tmp/nasal_eval_%ld_XXXXXX", std::time(nullptr));
|
||||
int fd = mkstemp(temp_filename);
|
||||
if (fd == -1) {
|
||||
throw std::runtime_error("Failed to create temporary file");
|
||||
}
|
||||
|
||||
// Write the code to the temporary file
|
||||
std::ofstream temp_file(temp_filename);
|
||||
if (!temp_file.is_open()) {
|
||||
close(fd);
|
||||
throw std::runtime_error("Failed to open temporary file for writing");
|
||||
}
|
||||
temp_file << code;
|
||||
temp_file.close();
|
||||
close(fd);
|
||||
|
||||
// Capture stdout and stderr
|
||||
std::stringstream output;
|
||||
std::stringstream error_output;
|
||||
auto old_cout = std::cout.rdbuf(output.rdbuf());
|
||||
auto old_cerr = std::cerr.rdbuf(error_output.rdbuf());
|
||||
|
||||
// Process the code
|
||||
if (lex.scan(std::string(temp_filename)).geterr()) {
|
||||
ctx->last_error = error_output.str();
|
||||
std::cout.rdbuf(old_cout);
|
||||
std::cerr.rdbuf(old_cerr);
|
||||
std::remove(temp_filename);
|
||||
return ctx->last_error.c_str();
|
||||
}
|
||||
|
||||
if (parse.compile(lex).geterr()) {
|
||||
ctx->last_error = error_output.str();
|
||||
std::cout.rdbuf(old_cout);
|
||||
std::cerr.rdbuf(old_cerr);
|
||||
std::remove(temp_filename);
|
||||
return ctx->last_error.c_str();
|
||||
}
|
||||
|
||||
ld.link(parse, false).chkerr();
|
||||
auto opt = std::make_unique<nasal::optimizer>();
|
||||
opt->do_optimization(parse.tree());
|
||||
gen.compile(parse, ld, false, true).chkerr();
|
||||
|
||||
const auto start = show_time ? clk::now() : clk::time_point();
|
||||
|
||||
// Create a future for the VM execution
|
||||
auto future = std::async(std::launch::async, [&]() {
|
||||
ctx->vm_instance->run(gen, ld, {});
|
||||
});
|
||||
|
||||
// Wait for completion or timeout
|
||||
auto status = future.wait_for(ctx->timeout);
|
||||
if (status == std::future_status::timeout) {
|
||||
std::remove(temp_filename);
|
||||
throw std::runtime_error("Execution timed out after " +
|
||||
std::to_string(ctx->timeout.count()) + " seconds");
|
||||
}
|
||||
|
||||
const auto end = show_time ? clk::now() : clk::time_point();
|
||||
|
||||
std::cout.rdbuf(old_cout);
|
||||
std::cerr.rdbuf(old_cerr);
|
||||
|
||||
std::stringstream result;
|
||||
result << output.str();
|
||||
if (!error_output.str().empty()) {
|
||||
result << error_output.str();
|
||||
}
|
||||
if (result.str().empty()) {
|
||||
result << "Execution completed successfully.\n";
|
||||
}
|
||||
|
||||
if (show_time) {
|
||||
double execution_time = static_cast<double>((end-start).count())/den;
|
||||
result << "\nExecution time: " << execution_time << "s";
|
||||
}
|
||||
|
||||
ctx->last_result = result.str();
|
||||
std::remove(temp_filename);
|
||||
|
||||
return ctx->last_result.c_str();
|
||||
} catch (const std::exception& e) {
|
||||
ctx->last_error = e.what();
|
||||
return ctx->last_error.c_str();
|
||||
}
|
||||
}
|
||||
|
||||
const char* nasal_get_error(void* context) {
|
||||
auto* ctx = static_cast<NasalContext*>(context);
|
||||
return ctx->last_error.c_str();
|
||||
}
|
||||
|
||||
void* nasal_repl_init() {
|
||||
auto* ctx = new WebReplContext();
|
||||
|
||||
try {
|
||||
// Initialize environment silently
|
||||
nasal::repl::info::instance()->in_repl_mode = true;
|
||||
ctx->repl_instance->get_runtime().set_repl_mode_flag(true);
|
||||
ctx->repl_instance->get_runtime().set_detail_report_info(false);
|
||||
|
||||
// Run initial setup
|
||||
ctx->repl_instance->set_source({});
|
||||
if (!ctx->repl_instance->run()) {
|
||||
ctx->last_error = "Failed to initialize REPL environment";
|
||||
return ctx;
|
||||
}
|
||||
|
||||
// Enable output after initialization
|
||||
ctx->allow_output = true;
|
||||
ctx->repl_instance->get_runtime().set_allow_repl_output_flag(true);
|
||||
ctx->initialized = true;
|
||||
} catch (const std::exception& e) {
|
||||
ctx->last_error = std::string("Initialization error: ") + e.what();
|
||||
}
|
||||
|
||||
return ctx;
|
||||
}
|
||||
|
||||
void nasal_repl_cleanup(void* context) {
|
||||
delete static_cast<WebReplContext*>(context);
|
||||
}
|
||||
|
||||
// Add new function to set REPL timeout
|
||||
void nasal_repl_set_timeout(void* context, int seconds) {
|
||||
auto* ctx = static_cast<WebReplContext*>(context);
|
||||
ctx->timeout = std::chrono::seconds(seconds);
|
||||
}
|
||||
|
||||
const char* nasal_repl_eval(void* context, const char* line) {
|
||||
auto* ctx = static_cast<WebReplContext*>(context);
|
||||
|
||||
if (!ctx->initialized) {
|
||||
ctx->last_error = "REPL not properly initialized";
|
||||
return ctx->last_error.c_str();
|
||||
}
|
||||
|
||||
try {
|
||||
std::string input_line(line);
|
||||
|
||||
// Handle empty input
|
||||
if (input_line.empty()) {
|
||||
ctx->last_result = "";
|
||||
return ctx->last_result.c_str();
|
||||
}
|
||||
|
||||
// Handle REPL commands
|
||||
if (input_line[0] == '.') {
|
||||
if (input_line == ".help" || input_line == ".h") {
|
||||
ctx->last_result =
|
||||
"Nasal REPL commands:\n"
|
||||
" .help .h show this help message\n"
|
||||
" .clear .c clear screen\n"
|
||||
" .exit .e exit repl\n"
|
||||
" .quit .q exit repl\n"
|
||||
" .source .s show source\n";
|
||||
return ctx->last_result.c_str();
|
||||
}
|
||||
else if (input_line == ".clear" || input_line == ".c") {
|
||||
ctx->last_result = "\033c"; // Special marker for clear screen
|
||||
return ctx->last_result.c_str();
|
||||
}
|
||||
else if (input_line == ".exit" || input_line == ".e" ||
|
||||
input_line == ".quit" || input_line == ".q") {
|
||||
ctx->last_result = "__EXIT__"; // Special marker for exit
|
||||
return ctx->last_result.c_str();
|
||||
}
|
||||
else if (input_line == ".source" || input_line == ".s") {
|
||||
// Return accumulated source
|
||||
ctx->last_result = ctx->source.empty() ?
|
||||
"(no source)" :
|
||||
join_string(ctx->source, "\n");
|
||||
return ctx->last_result.c_str();
|
||||
}
|
||||
else {
|
||||
ctx->last_error = "no such command \"" + input_line + "\", input \".help\" for help";
|
||||
return ctx->last_error.c_str();
|
||||
}
|
||||
}
|
||||
|
||||
// Add the line to source
|
||||
ctx->source.push_back(input_line);
|
||||
|
||||
// Capture output
|
||||
std::stringstream output;
|
||||
auto old_cout = std::cout.rdbuf(output.rdbuf());
|
||||
auto old_cerr = std::cerr.rdbuf(output.rdbuf());
|
||||
|
||||
// Create a copy of the source for the async task
|
||||
auto source_copy = ctx->source;
|
||||
|
||||
// Create a future for the REPL execution using the existing instance
|
||||
auto future = std::async(std::launch::async, [repl = ctx->repl_instance.get(), source_copy]() {
|
||||
repl->get_runtime().set_repl_mode_flag(true);
|
||||
repl->get_runtime().set_allow_repl_output_flag(true);
|
||||
repl->set_source(source_copy);
|
||||
return repl->run();
|
||||
});
|
||||
|
||||
// Wait for completion or timeout
|
||||
auto status = future.wait_for(ctx->timeout);
|
||||
|
||||
// Restore output streams first
|
||||
std::cout.rdbuf(old_cout);
|
||||
std::cerr.rdbuf(old_cerr);
|
||||
|
||||
if (status == std::future_status::timeout) {
|
||||
ctx->source.pop_back(); // Remove the line that caused timeout
|
||||
|
||||
// Reset the REPL instance state
|
||||
ctx->repl_instance->get_runtime().set_repl_mode_flag(true);
|
||||
ctx->repl_instance->get_runtime().set_allow_repl_output_flag(true);
|
||||
ctx->repl_instance->set_source(ctx->source);
|
||||
|
||||
throw std::runtime_error("Execution timed out after " +
|
||||
std::to_string(ctx->timeout.count()) + " seconds");
|
||||
}
|
||||
|
||||
bool success = future.get();
|
||||
std::string result = output.str();
|
||||
|
||||
if (!success) {
|
||||
ctx->last_error = result;
|
||||
ctx->source.pop_back(); // Remove failed line
|
||||
return ctx->last_error.c_str();
|
||||
}
|
||||
|
||||
ctx->last_result = result;
|
||||
return ctx->last_result.c_str();
|
||||
|
||||
} catch (const std::exception& e) {
|
||||
ctx->last_error = std::string("Error: ") + e.what();
|
||||
ctx->source.pop_back(); // Remove failed line
|
||||
return ctx->last_error.c_str();
|
||||
}
|
||||
}
|
||||
|
||||
int nasal_repl_is_complete(void* context, const char* line) {
|
||||
auto* ctx = static_cast<WebReplContext*>(context);
|
||||
|
||||
if (!ctx->initialized) {
|
||||
return -1; // Error state
|
||||
}
|
||||
|
||||
// Handle empty input
|
||||
if (!line || strlen(line) == 0) {
|
||||
return 0; // Complete
|
||||
}
|
||||
|
||||
// Handle REPL commands
|
||||
if (line[0] == '.') {
|
||||
return 0; // Commands are always complete
|
||||
}
|
||||
|
||||
// Create a temporary source vector with existing source plus new line
|
||||
std::vector<std::string> temp_source = ctx->source;
|
||||
temp_source.push_back(line);
|
||||
|
||||
// Use the REPL's check_need_more_input method
|
||||
int result = ctx->repl_instance->check_need_more_input(temp_source);
|
||||
return result; // Ensure a return value is provided
|
||||
}
|
||||
|
||||
// Add this function to expose version info
|
||||
const char* nasal_repl_get_version() {
|
||||
static std::string version_info =
|
||||
std::string("version ") + __nasver__ +
|
||||
" (" + __DATE__ + " " + __TIME__ + ")";
|
||||
return version_info.c_str();
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
#ifndef __NASAL_WEB_H__
|
||||
#define __NASAL_WEB_H__
|
||||
|
||||
#include "nasal.h"
|
||||
|
||||
#ifdef _WIN32
|
||||
#define NASAL_EXPORT __declspec(dllexport)
|
||||
#else
|
||||
#define NASAL_EXPORT __attribute__((visibility("default")))
|
||||
#endif
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
// Main API functions
|
||||
NASAL_EXPORT void* nasal_init();
|
||||
NASAL_EXPORT void nasal_cleanup(void* context);
|
||||
NASAL_EXPORT void nasal_set_timeout(void* context, int seconds);
|
||||
NASAL_EXPORT const char* nasal_eval(void* context, const char* code, int show_time);
|
||||
NASAL_EXPORT const char* nasal_get_error(void* context);
|
||||
|
||||
// REPL
|
||||
NASAL_EXPORT void* nasal_repl_init();
|
||||
NASAL_EXPORT void nasal_repl_cleanup(void* repl_context);
|
||||
NASAL_EXPORT void nasal_repl_set_timeout(void* repl_context, int seconds);
|
||||
NASAL_EXPORT const char* nasal_repl_eval(void* repl_context, const char* line);
|
||||
NASAL_EXPORT int nasal_repl_is_complete(void* repl_context, const char* line);
|
||||
NASAL_EXPORT const char* nasal_repl_get_version();
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
|
@ -34,6 +34,14 @@ void repl::update_temp_file() {
|
|||
info::instance()->repl_file_source = content + " ";
|
||||
}
|
||||
|
||||
void repl::update_temp_file(const std::vector<std::string>& src) {
|
||||
auto content = std::string("");
|
||||
for(const auto& i : src) {
|
||||
content += i + "\n";
|
||||
}
|
||||
info::instance()->repl_file_source = content + " ";
|
||||
}
|
||||
|
||||
bool repl::check_need_more_input() {
|
||||
while(true) {
|
||||
update_temp_file();
|
||||
|
@ -67,6 +75,34 @@ bool repl::check_need_more_input() {
|
|||
return true;
|
||||
}
|
||||
|
||||
int repl::check_need_more_input(std::vector<std::string>& src) {
|
||||
update_temp_file(src);
|
||||
auto nasal_lexer = std::make_unique<lexer>();
|
||||
if (nasal_lexer->scan("<nasal-repl>").geterr()) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
i64 in_curve = 0;
|
||||
i64 in_bracket = 0;
|
||||
i64 in_brace = 0;
|
||||
for(const auto& t : nasal_lexer->result()) {
|
||||
switch(t.type) {
|
||||
case tok::tk_lcurve: ++in_curve; break;
|
||||
case tok::tk_rcurve: --in_curve; break;
|
||||
case tok::tk_lbracket: ++in_bracket; break;
|
||||
case tok::tk_rbracket: --in_bracket; break;
|
||||
case tok::tk_lbrace: ++in_brace; break;
|
||||
case tok::tk_rbrace: --in_brace; break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
if (in_curve > 0 || in_bracket > 0 || in_brace > 0) {
|
||||
return 1; // More input needed
|
||||
}
|
||||
return 0; // Input is complete
|
||||
}
|
||||
|
||||
|
||||
void repl::help() {
|
||||
std::cout << ".h, .help | show help\n";
|
||||
std::cout << ".e, .exit | quit the REPL\n";
|
||||
|
@ -150,7 +186,7 @@ void repl::execute() {
|
|||
std::cout << "\", input \".help\" for help\n";
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
source.push_back(line);
|
||||
if (!check_need_more_input()) {
|
||||
source.pop_back();
|
||||
|
|
|
@ -36,8 +36,8 @@ private:
|
|||
std::string readline(const std::string&);
|
||||
bool check_need_more_input();
|
||||
void update_temp_file();
|
||||
void update_temp_file(const std::vector<std::string>& src);
|
||||
void help();
|
||||
bool run();
|
||||
|
||||
public:
|
||||
repl() {
|
||||
|
@ -48,7 +48,20 @@ public:
|
|||
// set empty history
|
||||
command_history = {""};
|
||||
}
|
||||
|
||||
// Make these methods public for web REPL
|
||||
bool run();
|
||||
void execute();
|
||||
int check_need_more_input(std::vector<std::string>& src);
|
||||
// Add method to access source
|
||||
void set_source(const std::vector<std::string>& src) {
|
||||
source = src;
|
||||
}
|
||||
|
||||
// Add method to access runtime
|
||||
vm& get_runtime() {
|
||||
return runtime;
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue