Add multi-file playground imports
This commit is contained in:
parent
19c9de1601
commit
4ab47e5719
55
Cargo.lock
generated
55
Cargo.lock
generated
|
|
@ -42,9 +42,16 @@ name = "decodal-wasm"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"decodal-core",
|
"decodal-core",
|
||||||
|
"serde_json",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "itoa"
|
||||||
|
version = "1.0.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "memchr"
|
name = "memchr"
|
||||||
version = "2.8.2"
|
version = "2.8.2"
|
||||||
|
|
@ -110,6 +117,48 @@ version = "1.0.22"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde"
|
||||||
|
version = "1.0.228"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
|
||||||
|
dependencies = [
|
||||||
|
"serde_core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_core"
|
||||||
|
version = "1.0.228"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
|
||||||
|
dependencies = [
|
||||||
|
"serde_derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_derive"
|
||||||
|
version = "1.0.228"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_json"
|
||||||
|
version = "1.0.150"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9"
|
||||||
|
dependencies = [
|
||||||
|
"itoa",
|
||||||
|
"memchr",
|
||||||
|
"serde",
|
||||||
|
"serde_core",
|
||||||
|
"zmij",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "2.0.117"
|
version = "2.0.117"
|
||||||
|
|
@ -171,3 +220,9 @@ checksum = "23939e44bb9a5d7576fa2b563dc2e136628f1224e88a8deed09e04858b77871f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zmij"
|
||||||
|
version = "1.0.21"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ crate-type = ["cdylib", "rlib"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
decodal-core = { path = "../decodal-core" }
|
decodal-core = { path = "../decodal-core" }
|
||||||
|
serde_json = "1"
|
||||||
wasm-bindgen = "0.2"
|
wasm-bindgen = "0.2"
|
||||||
|
|
||||||
[package.metadata.wasm-pack.profile.release]
|
[package.metadata.wasm-pack.profile.release]
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,22 @@
|
||||||
use decodal_core::{Data, EmptyLoader, Engine};
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
|
use decodal_core::{
|
||||||
|
Data, Diagnostic, DiagnosticKind, EmptyLoader, Engine, LoadedSource, SourceLoader, Span,
|
||||||
|
};
|
||||||
use wasm_bindgen::prelude::*;
|
use wasm_bindgen::prelude::*;
|
||||||
|
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
pub fn evaluate(source: &str) -> String {
|
pub fn evaluate(source: &str) -> String {
|
||||||
match evaluate_inner(source) {
|
encode_result(evaluate_inner(source))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen(js_name = evaluateProject)]
|
||||||
|
pub fn evaluate_project(entry: &str, files_json: &str) -> String {
|
||||||
|
encode_result(evaluate_project_inner(entry, files_json))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encode_result(result: Result<String, String>) -> String {
|
||||||
|
match result {
|
||||||
Ok(output) => format!("{{\"ok\":true,\"output\":{}}}", json_string(&output)),
|
Ok(output) => format!("{{\"ok\":true,\"output\":{}}}", json_string(&output)),
|
||||||
Err(error) => format!("{{\"ok\":false,\"error\":{}}}", json_string(&error)),
|
Err(error) => format!("{{\"ok\":false,\"error\":{}}}", json_string(&error)),
|
||||||
}
|
}
|
||||||
|
|
@ -19,6 +32,98 @@ fn evaluate_inner(source: &str) -> Result<String, String> {
|
||||||
Ok(format_data(&data, 0))
|
Ok(format_data(&data, 0))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn evaluate_project_inner(entry: &str, files_json: &str) -> Result<String, String> {
|
||||||
|
let raw_files: BTreeMap<String, String> = serde_json::from_str(files_json)
|
||||||
|
.map_err(|error| format!("failed to read playground files: {error}"))?;
|
||||||
|
let mut files = BTreeMap::new();
|
||||||
|
for (path, source) in raw_files {
|
||||||
|
let path = normalize_path(&path).ok_or_else(|| format!("invalid file path `{path}`"))?;
|
||||||
|
files.insert(path, source);
|
||||||
|
}
|
||||||
|
|
||||||
|
let entry = normalize_path(entry).ok_or_else(|| format!("invalid entry path `{entry}`"))?;
|
||||||
|
let source = files
|
||||||
|
.get(&entry)
|
||||||
|
.cloned()
|
||||||
|
.ok_or_else(|| format!("entry file `{entry}` was not found"))?;
|
||||||
|
|
||||||
|
let mut engine = Engine::new(VirtualLoader { files });
|
||||||
|
let module = engine
|
||||||
|
.add_root_source(entry.clone(), entry.clone(), &source)
|
||||||
|
.map_err(format_diagnostic)?;
|
||||||
|
let value = engine.eval_module(module).map_err(format_diagnostic)?;
|
||||||
|
let data = engine.materialize(&value).map_err(format_diagnostic)?;
|
||||||
|
Ok(format_data(&data, 0))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
struct VirtualLoader {
|
||||||
|
files: BTreeMap<String, String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SourceLoader for VirtualLoader {
|
||||||
|
fn load(
|
||||||
|
&mut self,
|
||||||
|
current_key: Option<&str>,
|
||||||
|
specifier: &str,
|
||||||
|
) -> decodal_core::Result<LoadedSource> {
|
||||||
|
let key = resolve_import(current_key, specifier).ok_or_else(|| {
|
||||||
|
Diagnostic::new(
|
||||||
|
DiagnosticKind::Import,
|
||||||
|
Span::default(),
|
||||||
|
format!("invalid import path `{specifier}`"),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
let source = self.files.get(&key).cloned().ok_or_else(|| {
|
||||||
|
Diagnostic::new(
|
||||||
|
DiagnosticKind::Import,
|
||||||
|
Span::default(),
|
||||||
|
format!("import `{specifier}` resolved to `{key}`, but that file does not exist"),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
Ok(LoadedSource {
|
||||||
|
key: key.clone(),
|
||||||
|
name: key,
|
||||||
|
source,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve_import(current_key: Option<&str>, specifier: &str) -> Option<String> {
|
||||||
|
if specifier.starts_with('/') {
|
||||||
|
return normalize_path(specifier);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut base = String::new();
|
||||||
|
if let Some(current_key) = current_key {
|
||||||
|
if let Some((parent, _file)) = current_key.rsplit_once('/') {
|
||||||
|
base.push_str(parent);
|
||||||
|
base.push('/');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
base.push_str(specifier);
|
||||||
|
normalize_path(&base)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn normalize_path(path: &str) -> Option<String> {
|
||||||
|
let mut parts = Vec::new();
|
||||||
|
let normalized = path.replace('\\', "/");
|
||||||
|
for part in normalized.split('/') {
|
||||||
|
match part {
|
||||||
|
"" | "." => {}
|
||||||
|
".." => {
|
||||||
|
parts.pop()?;
|
||||||
|
}
|
||||||
|
part => parts.push(part),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if parts.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(parts.join("/"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn format_diagnostic(diagnostic: decodal_core::Diagnostic) -> String {
|
fn format_diagnostic(diagnostic: decodal_core::Diagnostic) -> String {
|
||||||
format!(
|
format!(
|
||||||
"{:?} at {}:{}..{}: {}",
|
"{:?} at {}:{}..{}: {}",
|
||||||
|
|
@ -94,3 +199,35 @@ fn json_string(value: &str) -> String {
|
||||||
out.push('"');
|
out.push('"');
|
||||||
out
|
out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::{evaluate_project_inner, normalize_path, resolve_import};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn normalizes_virtual_paths() {
|
||||||
|
assert_eq!(
|
||||||
|
normalize_path("/schemas/../main.dcdl"),
|
||||||
|
Some("main.dcdl".into())
|
||||||
|
);
|
||||||
|
assert_eq!(normalize_path("../main.dcdl"), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn resolves_imports_relative_to_current_file() {
|
||||||
|
assert_eq!(
|
||||||
|
resolve_import(Some("schemas/service.dcdl"), "./types.dcdl"),
|
||||||
|
Some("schemas/types.dcdl".into())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn evaluates_project_imports() {
|
||||||
|
let files = r#"{
|
||||||
|
"main.dcdl":"let dep = import \"./schemas/service.dcdl\"; in dep.Service & { port = 9443; }",
|
||||||
|
"schemas/service.dcdl":"Service = { name = String default \"api\"; port = Int & > 443 default 8443; }"
|
||||||
|
}"#;
|
||||||
|
let output = evaluate_project_inner("main.dcdl", files).unwrap();
|
||||||
|
assert!(output.contains("\"port\": 9443"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
import { nav } from '../lib/docs.js';
|
import { nav } from '../lib/docs.js';
|
||||||
import '../style.css';
|
import '../style.css';
|
||||||
|
|
||||||
const { title = 'Decodal', active = '' } = Astro.props;
|
const { title = 'Decodal', active = '', playground = false } = Astro.props;
|
||||||
|
|
||||||
function renderNav(items) {
|
function renderNav(items) {
|
||||||
return `<ul class="nav-tree">${items
|
return `<ul class="nav-tree">${items
|
||||||
|
|
@ -30,12 +30,14 @@ function renderNav(items) {
|
||||||
<a href="/playground/">Playground</a>
|
<a href="/playground/">Playground</a>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
<div class="layout">
|
<div class={playground ? 'layout playground-layout' : 'layout'}>
|
||||||
<aside class="sidebar">
|
{!playground && (
|
||||||
<a class="sidebar-title" href="/docs/">Manual</a>
|
<aside class="sidebar">
|
||||||
<nav set:html={renderNav(nav)} />
|
<a class="sidebar-title" href="/docs/">Manual</a>
|
||||||
</aside>
|
<nav set:html={renderNav(nav)} />
|
||||||
<main class={Astro.props.playground ? 'playground' : ''}>
|
</aside>
|
||||||
|
)}
|
||||||
|
<main class={playground ? 'playground' : ''}>
|
||||||
<slot />
|
<slot />
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -6,19 +6,32 @@ import ManualLayout from '../layouts/ManualLayout.astro';
|
||||||
<div class="playground-header">
|
<div class="playground-header">
|
||||||
<div>
|
<div>
|
||||||
<h1>Playground</h1>
|
<h1>Playground</h1>
|
||||||
<p>Evaluate Decodal directly in your browser through WebAssembly.</p>
|
<p>Virtual files are evaluated in the browser through WebAssembly. Use import paths such as <code>./schemas/service.dcdl</code>.</p>
|
||||||
|
</div>
|
||||||
|
<div class="playground-actions">
|
||||||
|
<p id="status" class="status">Loading WASM...</p>
|
||||||
|
<button id="run" disabled>Run</button>
|
||||||
</div>
|
</div>
|
||||||
<button id="run" disabled>Run</button>
|
|
||||||
</div>
|
</div>
|
||||||
<p id="status" class="status">Loading WASM...</p>
|
|
||||||
<div class="playground-grid">
|
<div class="playground-shell">
|
||||||
|
<aside class="file-panel">
|
||||||
|
<div class="panel-header">
|
||||||
|
<span>Files</span>
|
||||||
|
<button id="new-file" type="button">New</button>
|
||||||
|
</div>
|
||||||
|
<div id="file-tree" class="file-tree"></div>
|
||||||
|
<button id="delete-file" class="danger-button" type="button">Delete file</button>
|
||||||
|
</aside>
|
||||||
|
|
||||||
<label class="pane input-pane">
|
<label class="pane input-pane">
|
||||||
<span>Input</span>
|
<span id="active-file">Input</span>
|
||||||
<div class="editor-wrap">
|
<div class="editor-wrap">
|
||||||
<pre id="source-highlight" aria-hidden="true"></pre>
|
<pre id="source-highlight" aria-hidden="true"></pre>
|
||||||
<textarea id="source" spellcheck="false"></textarea>
|
<textarea id="source" spellcheck="false"></textarea>
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<section class="pane output-pane">
|
<section class="pane output-pane">
|
||||||
<span>Output</span>
|
<span>Output</span>
|
||||||
<pre id="output"></pre>
|
<pre id="output"></pre>
|
||||||
|
|
|
||||||
|
|
@ -1,28 +1,91 @@
|
||||||
import init, { evaluate } from '../wasm/decodal_wasm.js';
|
import init, { evaluateProject } from '../wasm/decodal_wasm.js';
|
||||||
import { highlightDecodal } from '../lib/highlight.js';
|
import { highlightDecodal } from '../lib/highlight.js';
|
||||||
|
|
||||||
const starter = `let
|
const STORAGE_KEY = 'decodal-playground-project-v1';
|
||||||
Service = {
|
|
||||||
name = String;
|
const starterFiles = {
|
||||||
port = Int & > 443 default 8443;
|
'main.dcdl': `let
|
||||||
feature.enable = Bool default true;
|
schema = import "./schemas/service.dcdl";
|
||||||
};
|
|
||||||
in
|
in
|
||||||
Service & {
|
schema.Service & {
|
||||||
name = "api";
|
name = "api";
|
||||||
port = 9443;
|
port = 9443;
|
||||||
}
|
}
|
||||||
`;
|
`,
|
||||||
|
'schemas/service.dcdl': `Service = {
|
||||||
|
name = String;
|
||||||
|
port = Int & > 443 default 8443;
|
||||||
|
feature.enable = Bool default true;
|
||||||
|
};
|
||||||
|
`,
|
||||||
|
};
|
||||||
|
|
||||||
const source = document.getElementById('source');
|
const source = document.getElementById('source');
|
||||||
const sourceHighlight = document.getElementById('source-highlight');
|
const sourceHighlight = document.getElementById('source-highlight');
|
||||||
const output = document.getElementById('output');
|
const output = document.getElementById('output');
|
||||||
const run = document.getElementById('run');
|
const run = document.getElementById('run');
|
||||||
const status = document.getElementById('status');
|
const status = document.getElementById('status');
|
||||||
|
const fileTree = document.getElementById('file-tree');
|
||||||
|
const activeFile = document.getElementById('active-file');
|
||||||
|
const newFile = document.getElementById('new-file');
|
||||||
|
const deleteFile = document.getElementById('delete-file');
|
||||||
|
|
||||||
source.value = starter;
|
const project = loadProject();
|
||||||
|
|
||||||
|
setActiveFile(project.activePath);
|
||||||
|
renderFileTree();
|
||||||
updateHighlight();
|
updateHighlight();
|
||||||
|
|
||||||
|
function loadProject() {
|
||||||
|
try {
|
||||||
|
const stored = JSON.parse(localStorage.getItem(STORAGE_KEY) ?? 'null');
|
||||||
|
if (stored && stored.files && typeof stored.activePath === 'string') {
|
||||||
|
const files = normalizeFiles(stored.files);
|
||||||
|
const activePath = files[stored.activePath] === undefined ? Object.keys(files)[0] : stored.activePath;
|
||||||
|
if (activePath) return { files, activePath };
|
||||||
|
}
|
||||||
|
} catch (_error) {
|
||||||
|
// Fall back to the starter project.
|
||||||
|
}
|
||||||
|
return { files: { ...starterFiles }, activePath: 'main.dcdl' };
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeFiles(files) {
|
||||||
|
return Object.fromEntries(
|
||||||
|
Object.entries(files)
|
||||||
|
.filter(([_path, value]) => typeof value === 'string')
|
||||||
|
.map(([path, value]) => [normalizePath(path), value])
|
||||||
|
.filter(([path]) => path),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveProject() {
|
||||||
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(project));
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizePath(path) {
|
||||||
|
const parts = [];
|
||||||
|
for (const part of String(path).replaceAll('\\', '/').split('/')) {
|
||||||
|
if (!part || part === '.') continue;
|
||||||
|
if (part === '..') parts.pop();
|
||||||
|
else parts.push(part);
|
||||||
|
}
|
||||||
|
return parts.join('/');
|
||||||
|
}
|
||||||
|
|
||||||
|
function setActiveFile(path) {
|
||||||
|
const normalized = normalizePath(path);
|
||||||
|
if (project.files[normalized] === undefined) return;
|
||||||
|
project.activePath = normalized;
|
||||||
|
source.value = project.files[normalized];
|
||||||
|
activeFile.textContent = normalized;
|
||||||
|
deleteFile.disabled = Object.keys(project.files).length <= 1;
|
||||||
|
updateHighlight();
|
||||||
|
syncHighlightScroll();
|
||||||
|
renderFileTree();
|
||||||
|
saveProject();
|
||||||
|
}
|
||||||
|
|
||||||
function updateHighlight() {
|
function updateHighlight() {
|
||||||
sourceHighlight.innerHTML = `${highlightDecodal(source.value)}\n`;
|
sourceHighlight.innerHTML = `${highlightDecodal(source.value)}\n`;
|
||||||
}
|
}
|
||||||
|
|
@ -33,11 +96,63 @@ function syncHighlightScroll() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function execute() {
|
function execute() {
|
||||||
const result = JSON.parse(evaluate(source.value));
|
project.files[project.activePath] = source.value;
|
||||||
|
saveProject();
|
||||||
|
const result = JSON.parse(evaluateProject(project.activePath, JSON.stringify(project.files)));
|
||||||
output.textContent = result.ok ? result.output : result.error;
|
output.textContent = result.ok ? result.output : result.error;
|
||||||
output.classList.toggle('error', !result.ok);
|
output.classList.toggle('error', !result.ok);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderFileTree() {
|
||||||
|
const tree = buildTree(Object.keys(project.files).sort());
|
||||||
|
fileTree.replaceChildren(renderTreeList(tree.children));
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildTree(paths) {
|
||||||
|
const root = { name: '', children: new Map(), path: '' };
|
||||||
|
for (const path of paths) {
|
||||||
|
const parts = path.split('/');
|
||||||
|
let node = root;
|
||||||
|
let currentPath = '';
|
||||||
|
parts.forEach((part, index) => {
|
||||||
|
currentPath = currentPath ? `${currentPath}/${part}` : part;
|
||||||
|
if (!node.children.has(part)) {
|
||||||
|
node.children.set(part, { name: part, children: new Map(), path: currentPath, file: index + 1 === parts.length });
|
||||||
|
}
|
||||||
|
node = node.children.get(part);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderTreeList(children) {
|
||||||
|
const list = document.createElement('ul');
|
||||||
|
for (const child of [...children.values()].sort(compareNodes)) {
|
||||||
|
const item = document.createElement('li');
|
||||||
|
if (child.file) {
|
||||||
|
const button = document.createElement('button');
|
||||||
|
button.type = 'button';
|
||||||
|
button.className = child.path === project.activePath ? 'file active' : 'file';
|
||||||
|
button.textContent = child.name;
|
||||||
|
button.title = child.path;
|
||||||
|
button.addEventListener('click', () => setActiveFile(child.path));
|
||||||
|
item.append(button);
|
||||||
|
} else {
|
||||||
|
const label = document.createElement('span');
|
||||||
|
label.className = 'folder';
|
||||||
|
label.textContent = `${child.name}/`;
|
||||||
|
item.append(label, renderTreeList(child.children));
|
||||||
|
}
|
||||||
|
list.append(item);
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
function compareNodes(a, b) {
|
||||||
|
if (a.file !== b.file) return a.file ? 1 : -1;
|
||||||
|
return a.name.localeCompare(b.name);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await init();
|
await init();
|
||||||
run.disabled = false;
|
run.disabled = false;
|
||||||
|
|
@ -48,9 +163,27 @@ try {
|
||||||
}
|
}
|
||||||
|
|
||||||
run.addEventListener('click', execute);
|
run.addEventListener('click', execute);
|
||||||
|
newFile.addEventListener('click', () => {
|
||||||
|
const path = normalizePath(prompt('New virtual file path', 'schemas/types.dcdl') ?? '');
|
||||||
|
if (!path) return;
|
||||||
|
if (project.files[path] !== undefined) {
|
||||||
|
setActiveFile(path);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
project.files[path] = path.endsWith('.dcdl') ? '' : '// Decodal source\n';
|
||||||
|
setActiveFile(path);
|
||||||
|
});
|
||||||
|
deleteFile.addEventListener('click', () => {
|
||||||
|
if (Object.keys(project.files).length <= 1) return;
|
||||||
|
if (!confirm(`Delete ${project.activePath}?`)) return;
|
||||||
|
delete project.files[project.activePath];
|
||||||
|
setActiveFile(Object.keys(project.files).sort()[0]);
|
||||||
|
});
|
||||||
source.addEventListener('input', () => {
|
source.addEventListener('input', () => {
|
||||||
|
project.files[project.activePath] = source.value;
|
||||||
updateHighlight();
|
updateHighlight();
|
||||||
syncHighlightScroll();
|
syncHighlightScroll();
|
||||||
|
saveProject();
|
||||||
});
|
});
|
||||||
source.addEventListener('scroll', syncHighlightScroll);
|
source.addEventListener('scroll', syncHighlightScroll);
|
||||||
source.addEventListener('keydown', (event) => {
|
source.addEventListener('keydown', (event) => {
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,10 @@ a:hover {
|
||||||
min-height: calc(100vh - 56px);
|
min-height: calc(100vh - 56px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.playground-layout {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
.sidebar {
|
.sidebar {
|
||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
border-right: 1px solid #e5e7eb;
|
border-right: 1px solid #e5e7eb;
|
||||||
|
|
@ -94,7 +98,7 @@ main {
|
||||||
}
|
}
|
||||||
|
|
||||||
main.playground {
|
main.playground {
|
||||||
padding: 24px;
|
padding: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown {
|
.markdown {
|
||||||
|
|
@ -162,25 +166,41 @@ main.playground {
|
||||||
}
|
}
|
||||||
|
|
||||||
.playground-page {
|
.playground-page {
|
||||||
height: calc(100vh - 104px);
|
height: calc(100vh - 76px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.playground-header {
|
.playground-header {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
margin-bottom: 18px;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.playground-header h1 {
|
.playground-header h1 {
|
||||||
margin: 0 0 4px;
|
font-size: 20px;
|
||||||
|
margin: 0 0 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.playground-header p {
|
.playground-header p {
|
||||||
color: #4b5563;
|
color: #4b5563;
|
||||||
|
font-size: 13px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.playground-header code {
|
||||||
|
background: #eef2ff;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: #3730a3;
|
||||||
|
padding: 1px 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.playground-actions {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
background: #2563eb;
|
background: #2563eb;
|
||||||
border: 0;
|
border: 0;
|
||||||
|
|
@ -188,7 +208,7 @@ button {
|
||||||
color: white;
|
color: white;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
padding: 10px 18px;
|
padding: 8px 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
button:disabled {
|
button:disabled {
|
||||||
|
|
@ -196,32 +216,103 @@ button:disabled {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.playground-grid {
|
.playground-shell {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 16px;
|
gap: 10px;
|
||||||
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
grid-template-columns: 220px minmax(360px, 1.25fr) minmax(320px, 1fr);
|
||||||
height: calc(100% - 82px);
|
height: calc(100% - 52px);
|
||||||
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.file-panel,
|
||||||
.pane {
|
.pane {
|
||||||
background: white;
|
background: white;
|
||||||
border: 1px solid #e5e7eb;
|
border: 1px solid #e5e7eb;
|
||||||
border-radius: 12px;
|
border-radius: 10px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.panel-header,
|
||||||
.pane > span {
|
.pane > span {
|
||||||
|
align-items: center;
|
||||||
border-bottom: 1px solid #e5e7eb;
|
border-bottom: 1px solid #e5e7eb;
|
||||||
color: #374151;
|
color: #374151;
|
||||||
|
display: flex;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
padding: 10px 12px;
|
justify-content: space-between;
|
||||||
|
min-height: 38px;
|
||||||
|
padding: 0 10px;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.panel-header button {
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 5px 8px;
|
||||||
|
text-transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-tree {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
overflow: auto;
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-tree ul {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding-left: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-tree > ul {
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-tree li {
|
||||||
|
margin: 2px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-tree .folder {
|
||||||
|
color: #64748b;
|
||||||
|
display: block;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
|
padding: 4px 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-tree .file {
|
||||||
|
background: transparent;
|
||||||
|
border-radius: 6px;
|
||||||
|
color: #334155;
|
||||||
|
display: block;
|
||||||
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
padding: 5px 7px;
|
||||||
|
text-align: left;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-tree .file:hover,
|
||||||
|
.file-tree .file.active {
|
||||||
|
background: #dbeafe;
|
||||||
|
color: #1d4ed8;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.danger-button {
|
||||||
|
background: transparent;
|
||||||
|
border-top: 1px solid #e5e7eb;
|
||||||
|
border-radius: 0;
|
||||||
|
color: #b91c1c;
|
||||||
|
padding: 9px 10px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
.editor-wrap {
|
.editor-wrap {
|
||||||
background: #0f172a;
|
background: #0f172a;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
|
@ -237,7 +328,7 @@ button:disabled {
|
||||||
inset: 0;
|
inset: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
padding: 14px;
|
padding: 12px;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
tab-size: 2;
|
tab-size: 2;
|
||||||
white-space: pre;
|
white-space: pre;
|
||||||
|
|
@ -273,6 +364,9 @@ button:disabled {
|
||||||
|
|
||||||
.status {
|
.status {
|
||||||
color: #4b5563;
|
color: #4b5563;
|
||||||
|
font-size: 13px;
|
||||||
|
margin: 0;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 900px) {
|
@media (max-width: 900px) {
|
||||||
|
|
|
||||||
3
site/decodal-site/src/wasm/decodal_wasm.d.ts
vendored
3
site/decodal-site/src/wasm/decodal_wasm.d.ts
vendored
|
|
@ -3,11 +3,14 @@
|
||||||
|
|
||||||
export function evaluate(source: string): string;
|
export function evaluate(source: string): string;
|
||||||
|
|
||||||
|
export function evaluateProject(entry: string, files_json: string): string;
|
||||||
|
|
||||||
export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module;
|
export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module;
|
||||||
|
|
||||||
export interface InitOutput {
|
export interface InitOutput {
|
||||||
readonly memory: WebAssembly.Memory;
|
readonly memory: WebAssembly.Memory;
|
||||||
readonly evaluate: (a: number, b: number, c: number) => void;
|
readonly evaluate: (a: number, b: number, c: number) => void;
|
||||||
|
readonly evaluateProject: (a: number, b: number, c: number, d: number, e: number) => void;
|
||||||
readonly __wbindgen_add_to_stack_pointer: (a: number) => number;
|
readonly __wbindgen_add_to_stack_pointer: (a: number) => number;
|
||||||
readonly __wbindgen_export: (a: number, b: number) => number;
|
readonly __wbindgen_export: (a: number, b: number) => number;
|
||||||
readonly __wbindgen_export2: (a: number, b: number, c: number, d: number) => number;
|
readonly __wbindgen_export2: (a: number, b: number, c: number, d: number) => number;
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,32 @@ export function evaluate(source) {
|
||||||
wasm.__wbindgen_export3(deferred2_0, deferred2_1, 1);
|
wasm.__wbindgen_export3(deferred2_0, deferred2_1, 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} entry
|
||||||
|
* @param {string} files_json
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function evaluateProject(entry, files_json) {
|
||||||
|
let deferred3_0;
|
||||||
|
let deferred3_1;
|
||||||
|
try {
|
||||||
|
const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
|
||||||
|
const ptr0 = passStringToWasm0(entry, wasm.__wbindgen_export, wasm.__wbindgen_export2);
|
||||||
|
const len0 = WASM_VECTOR_LEN;
|
||||||
|
const ptr1 = passStringToWasm0(files_json, wasm.__wbindgen_export, wasm.__wbindgen_export2);
|
||||||
|
const len1 = WASM_VECTOR_LEN;
|
||||||
|
wasm.evaluateProject(retptr, ptr0, len0, ptr1, len1);
|
||||||
|
var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);
|
||||||
|
var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
|
||||||
|
deferred3_0 = r0;
|
||||||
|
deferred3_1 = r1;
|
||||||
|
return getStringFromWasm0(r0, r1);
|
||||||
|
} finally {
|
||||||
|
wasm.__wbindgen_add_to_stack_pointer(16);
|
||||||
|
wasm.__wbindgen_export3(deferred3_0, deferred3_1, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
function __wbg_get_imports() {
|
function __wbg_get_imports() {
|
||||||
const import0 = {
|
const import0 = {
|
||||||
__proto__: null,
|
__proto__: null,
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -2,6 +2,7 @@
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
export const memory: WebAssembly.Memory;
|
export const memory: WebAssembly.Memory;
|
||||||
export const evaluate: (a: number, b: number, c: number) => void;
|
export const evaluate: (a: number, b: number, c: number) => void;
|
||||||
|
export const evaluateProject: (a: number, b: number, c: number, d: number, e: number) => void;
|
||||||
export const __wbindgen_add_to_stack_pointer: (a: number) => number;
|
export const __wbindgen_add_to_stack_pointer: (a: number) => number;
|
||||||
export const __wbindgen_export: (a: number, b: number) => number;
|
export const __wbindgen_export: (a: number, b: number) => number;
|
||||||
export const __wbindgen_export2: (a: number, b: number, c: number, d: number) => number;
|
export const __wbindgen_export2: (a: number, b: number, c: number, d: number) => number;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user