Add canonical grammar and CodeMirror playground
This commit is contained in:
@@ -1,12 +1,13 @@
|
||||
import init, { evaluateProject, tokenizeSource } from '../wasm/decodal_wasm.js';
|
||||
import { highlightDecodal, highlightDecodalTokens } from '../lib/highlight.js';
|
||||
import init, { evaluateProject } from '../wasm/decodal_wasm.js';
|
||||
import { EditorView, basicSetup } from 'codemirror';
|
||||
import { keymap } from '@codemirror/view';
|
||||
import { decodal } from '../lib/codemirror/decodal.js';
|
||||
import { playgroundExamples } from './playground-examples.js';
|
||||
|
||||
const STORAGE_KEY = 'decodal-playground-project-v1';
|
||||
const starterProject = playgroundExamples[0];
|
||||
|
||||
const source = document.getElementById('source');
|
||||
const sourceHighlight = document.getElementById('source-highlight');
|
||||
const editorHost = document.getElementById('editor');
|
||||
const output = document.getElementById('output');
|
||||
const run = document.getElementById('run');
|
||||
const status = document.getElementById('status');
|
||||
@@ -18,7 +19,63 @@ const exampleSelect = document.getElementById('example-select');
|
||||
const loadExample = document.getElementById('load-example');
|
||||
|
||||
const project = loadProject();
|
||||
let wasmReady = false;
|
||||
|
||||
const editorTheme = EditorView.theme({
|
||||
'&': {
|
||||
backgroundColor: '#0f172a',
|
||||
color: '#e5e7eb',
|
||||
height: '100%',
|
||||
},
|
||||
'.cm-scroller': {
|
||||
fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace',
|
||||
fontSize: '14px',
|
||||
lineHeight: '1.5',
|
||||
overflow: 'auto',
|
||||
},
|
||||
'.cm-content': {
|
||||
caretColor: '#e5e7eb',
|
||||
minHeight: '100%',
|
||||
padding: '12px',
|
||||
},
|
||||
'.cm-gutters': {
|
||||
backgroundColor: '#111827',
|
||||
borderRight: '1px solid #334155',
|
||||
color: '#94a3b8',
|
||||
},
|
||||
'&.cm-focused': {
|
||||
outline: 'none',
|
||||
},
|
||||
'&.cm-focused .cm-cursor': {
|
||||
borderLeftColor: '#e5e7eb',
|
||||
},
|
||||
'&.cm-focused .cm-selectionBackground, .cm-selectionBackground, ::selection': {
|
||||
backgroundColor: 'rgb(59 130 246 / 0.35)',
|
||||
},
|
||||
}, { dark: true });
|
||||
|
||||
const editor = new EditorView({
|
||||
doc: project.files[project.activePath] ?? '',
|
||||
parent: editorHost,
|
||||
extensions: [
|
||||
basicSetup,
|
||||
decodal(),
|
||||
editorTheme,
|
||||
keymap.of([
|
||||
{
|
||||
key: 'Mod-Enter',
|
||||
run() {
|
||||
execute();
|
||||
return true;
|
||||
},
|
||||
},
|
||||
]),
|
||||
EditorView.updateListener.of((update) => {
|
||||
if (!update.docChanged) return;
|
||||
project.files[project.activePath] = update.state.doc.toString();
|
||||
saveProject();
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
for (const example of playgroundExamples) {
|
||||
const option = document.createElement('option');
|
||||
@@ -30,7 +87,6 @@ exampleSelect.value = starterProject.id;
|
||||
|
||||
setActiveFile(project.activePath);
|
||||
renderFileTree();
|
||||
updateHighlight();
|
||||
|
||||
function loadProject() {
|
||||
try {
|
||||
@@ -84,41 +140,36 @@ function normalizePath(path) {
|
||||
return parts.join('/');
|
||||
}
|
||||
|
||||
function getEditorText() {
|
||||
return editor.state.doc.toString();
|
||||
}
|
||||
|
||||
function setEditorText(value) {
|
||||
editor.dispatch({
|
||||
changes: {
|
||||
from: 0,
|
||||
to: editor.state.doc.length,
|
||||
insert: value,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function setActiveFile(path) {
|
||||
const normalized = normalizePath(path);
|
||||
if (project.files[normalized] === undefined) return;
|
||||
if (project.activePath && project.files[project.activePath] !== undefined) {
|
||||
project.files[project.activePath] = getEditorText();
|
||||
}
|
||||
project.activePath = normalized;
|
||||
source.value = project.files[normalized];
|
||||
setEditorText(project.files[normalized]);
|
||||
activeFile.textContent = normalized;
|
||||
deleteFile.disabled = Object.keys(project.files).length <= 1;
|
||||
updateHighlight();
|
||||
syncHighlightScroll();
|
||||
renderFileTree();
|
||||
saveProject();
|
||||
}
|
||||
|
||||
function updateHighlight() {
|
||||
sourceHighlight.innerHTML = `${highlightSource(source.value)}\n`;
|
||||
}
|
||||
|
||||
function highlightSource(value) {
|
||||
if (!wasmReady) return highlightDecodal(value);
|
||||
try {
|
||||
const result = JSON.parse(tokenizeSource(value));
|
||||
if (result.ok) return highlightDecodalTokens(value, result.tokens);
|
||||
} catch (_error) {
|
||||
// Fall back to the lightweight JavaScript highlighter.
|
||||
}
|
||||
return highlightDecodal(value);
|
||||
}
|
||||
|
||||
function syncHighlightScroll() {
|
||||
sourceHighlight.scrollTop = source.scrollTop;
|
||||
sourceHighlight.scrollLeft = source.scrollLeft;
|
||||
}
|
||||
|
||||
function execute() {
|
||||
project.files[project.activePath] = source.value;
|
||||
project.files[project.activePath] = getEditorText();
|
||||
saveProject();
|
||||
const result = JSON.parse(evaluateProject(project.activePath, JSON.stringify(project.files)));
|
||||
output.textContent = result.ok ? result.output : result.error;
|
||||
@@ -177,8 +228,6 @@ function compareNodes(a, b) {
|
||||
|
||||
try {
|
||||
await init();
|
||||
wasmReady = true;
|
||||
updateHighlight();
|
||||
run.disabled = false;
|
||||
status.textContent = '';
|
||||
execute();
|
||||
@@ -209,13 +258,3 @@ deleteFile.addEventListener('click', () => {
|
||||
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();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user