Add multi-file playground imports
This commit is contained in:
@@ -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) => {
|
||||
|
||||
Reference in New Issue
Block a user