import init, { evaluateProject } from '../wasm/decodal_wasm.js'; import { highlightDecodal } from '../lib/highlight.js'; const STORAGE_KEY = 'decodal-playground-project-v1'; const starterFiles = { 'main.dcdl': `let schema = import "./schemas/service.dcdl"; in schema.Service & { name = "api"; port = 9000 + 443; feature.enable = 9000 + 443 > 9000 && true; } `, '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'); 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`; } function syncHighlightScroll() { sourceHighlight.scrollTop = source.scrollTop; sourceHighlight.scrollLeft = source.scrollLeft; } function execute() { 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; status.textContent = ''; execute(); } catch (error) { status.textContent = `Failed to load WASM: ${error?.message ?? error}`; } 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) => { if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') execute(); });