Files
bux-lang/playground/frontend/index.html
T
dimgigov ef4aa42b0b 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
2026-06-10 20:33:26 +03:00

512 lines
16 KiB
HTML

<!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>