feat: add web playground with Docker sandbox
- playground/backend/main.go — Go HTTP server with /compile endpoint - playground/frontend/index.html — Monaco Editor with 8 code examples - playground/sandbox/Dockerfile — isolated Docker container with buxc2 - playground/docker-compose.yml — orchestrates backend + sandbox - playground/Makefile — build and run targets - playground/README.md — deployment guide for VPS with nginx + SSL
This commit is contained in:
@@ -0,0 +1,511 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Bux Playground</title>
|
||||
<link rel="stylesheet" data-name="vs/editor/editor.main" href="https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.45.0/min/vs/editor/editor.main.min.css">
|
||||
<style>
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body {
|
||||
font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
|
||||
background: #0d1117;
|
||||
color: #c9d1d9;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.header {
|
||||
background: #161b22;
|
||||
padding: 10px 20px;
|
||||
border-bottom: 1px solid #30363d;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.header .logo {
|
||||
font-weight: 700;
|
||||
color: #58a6ff;
|
||||
font-size: 18px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.header .logo::before {
|
||||
content: '◈';
|
||||
color: #7ee787;
|
||||
}
|
||||
.header .version {
|
||||
color: #8b949e;
|
||||
font-size: 12px;
|
||||
}
|
||||
.header .examples {
|
||||
margin-left: auto;
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
}
|
||||
.header select {
|
||||
background: #21262d;
|
||||
color: #c9d1d9;
|
||||
border: 1px solid #30363d;
|
||||
border-radius: 6px;
|
||||
padding: 4px 10px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.main {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
.editor-panel {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
}
|
||||
.toolbar {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
padding: 8px 16px;
|
||||
background: #161b22;
|
||||
border-bottom: 1px solid #30363d;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.toolbar button {
|
||||
padding: 6px 16px;
|
||||
border: 1px solid #30363d;
|
||||
background: #21262d;
|
||||
color: #c9d1d9;
|
||||
cursor: pointer;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
font-family: inherit;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
.toolbar button:hover {
|
||||
background: #30363d;
|
||||
}
|
||||
.toolbar button.run {
|
||||
background: #238636;
|
||||
color: #fff;
|
||||
border-color: #238636;
|
||||
font-weight: 600;
|
||||
}
|
||||
.toolbar button.run:hover {
|
||||
background: #2ea043;
|
||||
}
|
||||
.toolbar button.run:disabled {
|
||||
background: #1f4d2e;
|
||||
border-color: #1f4d2e;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
#editor {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
.output-panel {
|
||||
width: 45%;
|
||||
min-width: 300px;
|
||||
max-width: 600px;
|
||||
background: #0d1117;
|
||||
border-left: 1px solid #30363d;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.output-header {
|
||||
padding: 8px 16px;
|
||||
background: #161b22;
|
||||
border-bottom: 1px solid #30363d;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #8b949e;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
#output {
|
||||
flex: 1;
|
||||
padding: 12px 16px;
|
||||
font-family: 'SFMono-Regular', 'Fira Code', 'JetBrains Mono', Consolas, monospace;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
overflow-y: auto;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
color: #c9d1d9;
|
||||
}
|
||||
#output.error { color: #f85149; }
|
||||
#output.success { color: #7ee787; }
|
||||
#output.running { color: #8b949e; }
|
||||
.status-bar {
|
||||
padding: 4px 16px;
|
||||
background: #161b22;
|
||||
border-top: 1px solid #30363d;
|
||||
font-size: 11px;
|
||||
color: #8b949e;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.spinner {
|
||||
display: inline-block;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border: 2px solid #30363d;
|
||||
border-top-color: #58a6ff;
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
margin-right: 6px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
@keyframes spin { to { transform: rotate(360deg); } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<span class="logo">Bux Playground</span>
|
||||
<span class="version">Try the Bux programming language in your browser</span>
|
||||
<div class="examples">
|
||||
<select id="exampleSelect">
|
||||
<option value="">Load example...</option>
|
||||
<option value="hello">Hello World</option>
|
||||
<option value="factorial">Factorial</option>
|
||||
<option value="fibonacci">Fibonacci</option>
|
||||
<option value="structs">Structs & Methods</option>
|
||||
<option value="generics">Generics</option>
|
||||
<option value="channels">Channels</option>
|
||||
<option value="async">Async/Await</option>
|
||||
<option value="result">Error Handling</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="main">
|
||||
<div class="editor-panel">
|
||||
<div class="toolbar">
|
||||
<button class="run" id="runBtn" onclick="runCode()">▶ Run</button>
|
||||
<button onclick="clearOutput()">Clear Output</button>
|
||||
<button onclick="formatCode()">Format</button>
|
||||
</div>
|
||||
<div id="editor"></div>
|
||||
</div>
|
||||
<div class="output-panel">
|
||||
<div class="output-header">Output</div>
|
||||
<div id="output"></div>
|
||||
<div class="status-bar">
|
||||
<span>Ctrl+Enter to run</span>
|
||||
<span id="statusText">Ready</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.45.0/min/vs/loader.min.js"></script>
|
||||
<script>
|
||||
const examples = {
|
||||
hello: `import Std::Io::PrintLine;
|
||||
|
||||
func Main() -> int {
|
||||
PrintLine("Hello, Bux!");
|
||||
return 0;
|
||||
}`,
|
||||
factorial: `import Std::Io::{PrintLine, PrintInt};
|
||||
import Std::Fmt::Fmt_Fmt2;
|
||||
|
||||
func Factorial(n: int) -> int {
|
||||
if n <= 1 {
|
||||
return 1;
|
||||
}
|
||||
return n * Factorial(n - 1);
|
||||
}
|
||||
|
||||
func Main() -> int {
|
||||
PrintLine("Factorials:");
|
||||
var i: int = 1;
|
||||
while i <= 10 {
|
||||
let fact: int = Factorial(i);
|
||||
PrintLine(Fmt_Fmt2("{0}! = {1}", String_FromInt(i), String_FromInt(fact)));
|
||||
i = i + 1;
|
||||
}
|
||||
return 0;
|
||||
}`,
|
||||
fibonacci: `import Std::Io::{PrintLine, PrintInt};
|
||||
|
||||
func Fibonacci(n: int) -> int {
|
||||
if n <= 1 {
|
||||
return n;
|
||||
}
|
||||
return Fibonacci(n - 1) + Fibonacci(n - 2);
|
||||
}
|
||||
|
||||
func Main() -> int {
|
||||
PrintLine("Fibonacci sequence:");
|
||||
var i: int = 0;
|
||||
while i < 15 {
|
||||
PrintInt(Fibonacci(i));
|
||||
PrintLine("");
|
||||
i = i + 1;
|
||||
}
|
||||
return 0;
|
||||
}`,
|
||||
structs: `import Std::Io::{PrintLine, PrintInt};
|
||||
|
||||
struct Rectangle {
|
||||
width: int;
|
||||
height: int;
|
||||
}
|
||||
|
||||
extend Rectangle {
|
||||
func Area(self: Rectangle) -> int {
|
||||
return self.width * self.height;
|
||||
}
|
||||
func Perimeter(self: Rectangle) -> int {
|
||||
return 2 * (self.width + self.height);
|
||||
}
|
||||
}
|
||||
|
||||
func Main() -> int {
|
||||
let rect: Rectangle = Rectangle { width: 10, height: 5 };
|
||||
PrintLine("Rectangle:");
|
||||
PrintLine(Fmt_Fmt2(" Area = {0}", String_FromInt(rect.Area())));
|
||||
PrintLine(Fmt_Fmt2(" Perimeter = {0}", String_FromInt(rect.Perimeter())));
|
||||
return 0;
|
||||
}`,
|
||||
generics: `import Std::Io::{PrintLine, PrintInt};
|
||||
|
||||
func Max<T>(a: T, b: T) -> T {
|
||||
if a > b {
|
||||
return a;
|
||||
}
|
||||
return b;
|
||||
}
|
||||
|
||||
func Main() -> int {
|
||||
let m1: int = Max<int>(10, 20);
|
||||
PrintLine("Max of 10 and 20:");
|
||||
PrintInt(m1);
|
||||
PrintLine("");
|
||||
return 0;
|
||||
}`,
|
||||
channels: `import Std::Io::{PrintLine, PrintInt};
|
||||
import Std::Channel::{Channel, Channel_New, Channel_SendInt, Channel_RecvInt, Channel_Close};
|
||||
|
||||
func Producer(chPtr: *Channel<int>) {
|
||||
var i: int = 1;
|
||||
while i <= 5 {
|
||||
Channel_SendInt(chPtr, i * 10);
|
||||
i = i + 1;
|
||||
}
|
||||
Channel_Close<int>(chPtr);
|
||||
}
|
||||
|
||||
func Consumer(chPtr: *Channel<int>) {
|
||||
var total: int = 0;
|
||||
while true {
|
||||
let val: int = Channel_RecvInt(chPtr);
|
||||
if val == 0 { break; }
|
||||
total = total + val;
|
||||
PrintInt(val);
|
||||
PrintLine("");
|
||||
}
|
||||
PrintLine("Total:");
|
||||
PrintInt(total);
|
||||
PrintLine("");
|
||||
}
|
||||
|
||||
func Main() -> int {
|
||||
let ch: Channel<int> = Channel_New<int>(3);
|
||||
let p: *void = spawn Producer(&ch);
|
||||
let c: *void = spawn Consumer(&ch);
|
||||
return 0;
|
||||
}`,
|
||||
async: `import Std::Io::{PrintLine, PrintInt};
|
||||
|
||||
async func Compute() -> int {
|
||||
PrintLine("Compute: step 1");
|
||||
bux_async_yield();
|
||||
PrintLine("Compute: step 2");
|
||||
return 42;
|
||||
}
|
||||
|
||||
func Main() -> int {
|
||||
let h1 = spawn Compute();
|
||||
let r1: int = h1.await as int;
|
||||
PrintLine("Result:");
|
||||
PrintInt(r1);
|
||||
PrintLine("");
|
||||
return 0;
|
||||
}`,
|
||||
result: `import Std::Io::PrintLine;
|
||||
|
||||
enum Result {
|
||||
Ok(int),
|
||||
Err(String)
|
||||
}
|
||||
|
||||
func Divide(a: int, b: int) -> Result {
|
||||
if b == 0 {
|
||||
return Result_NewErr("division by zero");
|
||||
}
|
||||
return Result_NewOk(a / b);
|
||||
}
|
||||
|
||||
func Compute() -> Result {
|
||||
let x: int = Divide(10, 2)?;
|
||||
let y: int = Divide(x, 5)?;
|
||||
return Result_NewOk(y);
|
||||
}
|
||||
|
||||
func Main() -> int {
|
||||
let result: Result = Compute();
|
||||
match result {
|
||||
case Ok(val) => PrintLine("Success!");
|
||||
case Err(msg) => PrintLine("Error!");
|
||||
}
|
||||
return 0;
|
||||
}`
|
||||
};
|
||||
|
||||
// Monaco Editor setup
|
||||
require.config({ paths: { vs: 'https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.45.0/min/vs' } });
|
||||
let editor;
|
||||
require(['vs/editor/editor.main'], function () {
|
||||
// Simple Bux language definition
|
||||
monaco.languages.register({ id: 'bux' });
|
||||
monaco.languages.setMonarchTokensProvider('bux', {
|
||||
keywords: [
|
||||
'func', 'let', 'var', 'const', 'type', 'struct', 'enum', 'union',
|
||||
'interface', 'extend', 'module', 'import', 'pub', 'extern',
|
||||
'if', 'else', 'while', 'do', 'loop', 'for', 'in', 'break',
|
||||
'continue', 'return', 'match', 'as', 'is', 'null', 'self',
|
||||
'super', 'sizeof', 'async', 'await', 'spawn', 'defer',
|
||||
'switch', 'case', 'default', 'checked'
|
||||
],
|
||||
operators: [
|
||||
'=', '>', '<', '!', '~', '?', ':', '==', '<=', '>=', '!=',
|
||||
'&&', '||', '++', '--', '+', '-', '*', '/', '&', '|', '^',
|
||||
'%', '<<', '>>', '>>>', '+=', '-=', '*=', '/=', '&=', '|=',
|
||||
'^=', '%=', '<<=', '>>=', '>>>='
|
||||
],
|
||||
symbols: /[=><!~?:&|+\-*\/\^%]+/,
|
||||
tokenizer: {
|
||||
root: [
|
||||
[/[a-zA-Z_]\w*/, {
|
||||
cases: {
|
||||
'@keywords': 'keyword',
|
||||
'@default': 'identifier'
|
||||
}
|
||||
}],
|
||||
[/\d+/, 'number'],
|
||||
[/"[^"]*"/, 'string'],
|
||||
[/`[^`]*`/, 'string'],
|
||||
[/[{}()\[\]]/, '@brackets'],
|
||||
[/@symbols/, {
|
||||
cases: {
|
||||
'@operators': 'operator',
|
||||
'@default': ''
|
||||
}
|
||||
}],
|
||||
[/\/\/.*$/, 'comment'],
|
||||
[/\/\*/, 'comment', '@comment'],
|
||||
],
|
||||
comment: [
|
||||
[/[^\/*]+/, 'comment'],
|
||||
[/\*\//, 'comment', '@pop'],
|
||||
[/[\/*]/, 'comment']
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
editor = monaco.editor.create(document.getElementById('editor'), {
|
||||
value: examples.hello,
|
||||
language: 'bux',
|
||||
theme: 'vs-dark',
|
||||
fontSize: 14,
|
||||
fontFamily: "'Fira Code', 'JetBrains Mono', monospace",
|
||||
minimap: { enabled: false },
|
||||
scrollBeyondLastLine: false,
|
||||
automaticLayout: true,
|
||||
tabSize: 4,
|
||||
insertSpaces: false,
|
||||
lineNumbers: 'on',
|
||||
renderWhitespace: 'selection',
|
||||
folding: true,
|
||||
wordWrap: 'on'
|
||||
});
|
||||
|
||||
// Ctrl+Enter shortcut
|
||||
editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter, runCode);
|
||||
});
|
||||
|
||||
// Example selector
|
||||
document.getElementById('exampleSelect').addEventListener('change', function(e) {
|
||||
const key = e.target.value;
|
||||
if (key && examples[key] && editor) {
|
||||
editor.setValue(examples[key]);
|
||||
}
|
||||
e.target.value = '';
|
||||
});
|
||||
|
||||
const outputDiv = document.getElementById('output');
|
||||
const runBtn = document.getElementById('runBtn');
|
||||
const statusText = document.getElementById('statusText');
|
||||
|
||||
async function runCode() {
|
||||
if (!editor) return;
|
||||
const code = editor.getValue();
|
||||
outputDiv.textContent = '';
|
||||
outputDiv.className = 'output running';
|
||||
runBtn.disabled = true;
|
||||
statusText.innerHTML = '<span class="spinner"></span>Compiling...';
|
||||
|
||||
try {
|
||||
const resp = await fetch('/compile', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'text/plain' },
|
||||
body: code
|
||||
});
|
||||
const data = await resp.json();
|
||||
|
||||
let output = data.output || '';
|
||||
// Filter verbose build lines
|
||||
let lines = output.split('\n');
|
||||
let filtered = [];
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
let line = lines[i];
|
||||
if (line.includes('[Package]') || line.includes('[Build]') ||
|
||||
line.includes('Hint:') || line.includes('Bux compiler') ||
|
||||
line.startsWith('Compiling') || line.startsWith('Linking') ||
|
||||
line.startsWith('Build successful')) continue;
|
||||
filtered.push(line);
|
||||
}
|
||||
output = filtered.join('\n').trim() || '(no output)';
|
||||
|
||||
outputDiv.textContent = output;
|
||||
outputDiv.className = 'output' + (data.isError ? ' error' : ' success');
|
||||
statusText.textContent = data.isError ? 'Error' : 'Done';
|
||||
} catch (e) {
|
||||
outputDiv.textContent = 'Error: ' + e.message;
|
||||
outputDiv.className = 'output error';
|
||||
statusText.textContent = 'Error';
|
||||
} finally {
|
||||
runBtn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
function clearOutput() {
|
||||
outputDiv.textContent = '';
|
||||
outputDiv.className = 'output';
|
||||
statusText.textContent = 'Ready';
|
||||
}
|
||||
|
||||
function formatCode() {
|
||||
if (!editor) return;
|
||||
// Simple auto-indent (Monaco formatter would be better)
|
||||
const code = editor.getValue();
|
||||
editor.setValue(code);
|
||||
statusText.textContent = 'Formatted';
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user