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"
|
||||
dependencies = [
|
||||
"decodal-core",
|
||||
"serde_json",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.8.2"
|
||||
|
|
@ -110,6 +117,48 @@ version = "1.0.22"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "syn"
|
||||
version = "2.0.117"
|
||||
|
|
@ -171,3 +220,9 @@ checksum = "23939e44bb9a5d7576fa2b563dc2e136628f1224e88a8deed09e04858b77871f"
|
|||
dependencies = [
|
||||
"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]
|
||||
decodal-core = { path = "../decodal-core" }
|
||||
serde_json = "1"
|
||||
wasm-bindgen = "0.2"
|
||||
|
||||
[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::*;
|
||||
|
||||
#[wasm_bindgen]
|
||||
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)),
|
||||
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))
|
||||
}
|
||||
|
||||
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 {
|
||||
format!(
|
||||
"{:?} at {}:{}..{}: {}",
|
||||
|
|
@ -94,3 +199,35 @@ fn json_string(value: &str) -> String {
|
|||
out.push('"');
|
||||
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 '../style.css';
|
||||
|
||||
const { title = 'Decodal', active = '' } = Astro.props;
|
||||
const { title = 'Decodal', active = '', playground = false } = Astro.props;
|
||||
|
||||
function renderNav(items) {
|
||||
return `<ul class="nav-tree">${items
|
||||
|
|
@ -30,12 +30,14 @@ function renderNav(items) {
|
|||
<a href="/playground/">Playground</a>
|
||||
</nav>
|
||||
</header>
|
||||
<div class="layout">
|
||||
<aside class="sidebar">
|
||||
<a class="sidebar-title" href="/docs/">Manual</a>
|
||||
<nav set:html={renderNav(nav)} />
|
||||
</aside>
|
||||
<main class={Astro.props.playground ? 'playground' : ''}>
|
||||
<div class={playground ? 'layout playground-layout' : 'layout'}>
|
||||
{!playground && (
|
||||
<aside class="sidebar">
|
||||
<a class="sidebar-title" href="/docs/">Manual</a>
|
||||
<nav set:html={renderNav(nav)} />
|
||||
</aside>
|
||||
)}
|
||||
<main class={playground ? 'playground' : ''}>
|
||||
<slot />
|
||||
</main>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -6,19 +6,32 @@ import ManualLayout from '../layouts/ManualLayout.astro';
|
|||
<div class="playground-header">
|
||||
<div>
|
||||
<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>
|
||||
<button id="run" disabled>Run</button>
|
||||
</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">
|
||||
<span>Input</span>
|
||||
<span id="active-file">Input</span>
|
||||
<div class="editor-wrap">
|
||||
<pre id="source-highlight" aria-hidden="true"></pre>
|
||||
<textarea id="source" spellcheck="false"></textarea>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<section class="pane output-pane">
|
||||
<span>Output</span>
|
||||
<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';
|
||||
|
||||
const starter = `let
|
||||
Service = {
|
||||
name = String;
|
||||
port = Int & > 443 default 8443;
|
||||
feature.enable = Bool default true;
|
||||
};
|
||||
const STORAGE_KEY = 'decodal-playground-project-v1';
|
||||
|
||||
const starterFiles = {
|
||||
'main.dcdl': `let
|
||||
schema = import "./schemas/service.dcdl";
|
||||
in
|
||||
Service & {
|
||||
schema.Service & {
|
||||
name = "api";
|
||||
port = 9443;
|
||||
}
|
||||
`;
|
||||
`,
|
||||
'schemas/service.dcdl': `Service = {
|
||||
name = String;
|
||||
port = Int & > 443 default 8443;
|
||||
feature.enable = Bool default true;
|
||||
};
|
||||
`,
|
||||
};
|
||||
|
||||
const source = document.getElementById('source');
|
||||
const sourceHighlight = document.getElementById('source-highlight');
|
||||
const output = document.getElementById('output');
|
||||
const run = document.getElementById('run');
|
||||
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();
|
||||
|
||||
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() {
|
||||
sourceHighlight.innerHTML = `${highlightDecodal(source.value)}\n`;
|
||||
}
|
||||
|
|
@ -33,11 +96,63 @@ function syncHighlightScroll() {
|
|||
}
|
||||
|
||||
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.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 {
|
||||
await init();
|
||||
run.disabled = false;
|
||||
|
|
@ -48,9 +163,27 @@ try {
|
|||
}
|
||||
|
||||
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', () => {
|
||||
project.files[project.activePath] = source.value;
|
||||
updateHighlight();
|
||||
syncHighlightScroll();
|
||||
saveProject();
|
||||
});
|
||||
source.addEventListener('scroll', syncHighlightScroll);
|
||||
source.addEventListener('keydown', (event) => {
|
||||
|
|
|
|||
|
|
@ -49,6 +49,10 @@ a:hover {
|
|||
min-height: calc(100vh - 56px);
|
||||
}
|
||||
|
||||
.playground-layout {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
background: #ffffff;
|
||||
border-right: 1px solid #e5e7eb;
|
||||
|
|
@ -94,7 +98,7 @@ main {
|
|||
}
|
||||
|
||||
main.playground {
|
||||
padding: 24px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.markdown {
|
||||
|
|
@ -162,25 +166,41 @@ main.playground {
|
|||
}
|
||||
|
||||
.playground-page {
|
||||
height: calc(100vh - 104px);
|
||||
height: calc(100vh - 76px);
|
||||
}
|
||||
|
||||
.playground-header {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 18px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.playground-header h1 {
|
||||
margin: 0 0 4px;
|
||||
font-size: 20px;
|
||||
margin: 0 0 2px;
|
||||
}
|
||||
|
||||
.playground-header p {
|
||||
color: #4b5563;
|
||||
font-size: 13px;
|
||||
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 {
|
||||
background: #2563eb;
|
||||
border: 0;
|
||||
|
|
@ -188,7 +208,7 @@ button {
|
|||
color: white;
|
||||
cursor: pointer;
|
||||
font-weight: 700;
|
||||
padding: 10px 18px;
|
||||
padding: 8px 14px;
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
|
|
@ -196,32 +216,103 @@ button:disabled {
|
|||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.playground-grid {
|
||||
.playground-shell {
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
||||
height: calc(100% - 82px);
|
||||
gap: 10px;
|
||||
grid-template-columns: 220px minmax(360px, 1.25fr) minmax(320px, 1fr);
|
||||
height: calc(100% - 52px);
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.file-panel,
|
||||
.pane {
|
||||
background: white;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 12px;
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.panel-header,
|
||||
.pane > span {
|
||||
align-items: center;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
color: #374151;
|
||||
display: flex;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
padding: 10px 12px;
|
||||
justify-content: space-between;
|
||||
min-height: 38px;
|
||||
padding: 0 10px;
|
||||
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 {
|
||||
background: #0f172a;
|
||||
flex: 1;
|
||||
|
|
@ -237,7 +328,7 @@ button:disabled {
|
|||
inset: 0;
|
||||
margin: 0;
|
||||
overflow: auto;
|
||||
padding: 14px;
|
||||
padding: 12px;
|
||||
position: absolute;
|
||||
tab-size: 2;
|
||||
white-space: pre;
|
||||
|
|
@ -273,6 +364,9 @@ button:disabled {
|
|||
|
||||
.status {
|
||||
color: #4b5563;
|
||||
font-size: 13px;
|
||||
margin: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@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 evaluateProject(entry: string, files_json: string): string;
|
||||
|
||||
export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module;
|
||||
|
||||
export interface InitOutput {
|
||||
readonly memory: WebAssembly.Memory;
|
||||
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_export: (a: number, b: 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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @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() {
|
||||
const import0 = {
|
||||
__proto__: null,
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -2,6 +2,7 @@
|
|||
/* eslint-disable */
|
||||
export const memory: WebAssembly.Memory;
|
||||
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_export: (a: number, b: number) => number;
|
||||
export const __wbindgen_export2: (a: number, b: number, c: number, d: number) => number;
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user