import { getExample } from './examples.js';
import * as Parser from './parser.js';
import * as Editor from './editor.js';
import * as Renderer from './renderer.js';
import * as TreeViz from './tree-viz.js';
const state = {
currentFileType: 'module',
autoParse: true,
parseTimeout: null,
lastParseResult: null,
isInitialized: false,
activeTab: 'tokens'
};
async function init() {
console.log('🚀 Initializing VB6Parse Playground...');
try {
showLoading('Initializing WASM module...');
const wasmOk = await Parser.initWasm();
if (!wasmOk) {
throw new Error('Failed to initialize WASM module');
}
await Editor.initEditor('editor-container');
TreeViz.initTreeViz('tree-viz-container');
setupEventListeners();
loadFromLocalStorage();
hideLoading();
state.isInitialized = true;
console.log('✅ Playground initialized successfully');
} catch (error) {
console.error('❌ Initialization failed:', error);
showError(`Failed to initialize playground: ${error.message}`);
hideLoading();
}
}
function setupEventListeners() {
document.getElementById('file-type')?.addEventListener('change', handleFileTypeChange);
document.getElementById('examples')?.addEventListener('change', handleExampleChange);
document.getElementById('parse-btn')?.addEventListener('click', handleParse);
document.getElementById('share-btn')?.addEventListener('click', handleShare);
document.getElementById('clear-btn')?.addEventListener('click', handleClear);
document.getElementById('auto-parse')?.addEventListener('change', handleAutoParseToggle);
document.querySelectorAll('.tab-btn').forEach(btn => {
btn.addEventListener('click', () => handleTabChange(btn.dataset.tab));
});
document.addEventListener('highlightNodeInEditor', handleHighlightNode);
document.addEventListener('highlightAndPositionCursor', handleHighlightAndPosition);
document.addEventListener('clearEditorHighlight', handleClearHighlight);
document.getElementById('show-whitespace')?.addEventListener('change', () => {
if (state.lastParseResult) {
Renderer.renderTokensTab(state.lastParseResult.tokens);
}
});
document.getElementById('token-filter')?.addEventListener('change', handleTokenFilter);
document.getElementById('token-search')?.addEventListener('input', handleTokenSearch);
document.getElementById('expand-all')?.addEventListener('click', handleExpandAll);
document.getElementById('collapse-all')?.addEventListener('click', handleCollapseAll);
document.getElementById('show-byte-ranges')?.addEventListener('change', () => {
if (state.lastParseResult) {
Renderer.renderCstTab(state.lastParseResult.cst);
}
});
document.getElementById('tree-layout-toggle')?.addEventListener('click', TreeViz.toggleLayout);
document.getElementById('tree-fit')?.addEventListener('click', TreeViz.fitToScreen);
document.getElementById('tree-reset-zoom')?.addEventListener('click', TreeViz.resetZoom);
document.addEventListener('editorContentChanged', handleEditorChange);
document.addEventListener('editorCursorPositionChange', handleEditorCursorChange);
document.addEventListener('highlightInEditor', handleHighlightRequest);
document.getElementById('theme-toggle')?.addEventListener('click', handleThemeToggle);
setupResizer();
window.addEventListener('resize', handleWindowResize);
console.log('✅ Event listeners set up');
}
function handleFileTypeChange(e) {
state.currentFileType = e.target.value;
Editor.setFileType(state.currentFileType);
console.log(`📄 File type changed to: ${state.currentFileType}`);
if (state.autoParse) {
debouncedParse();
}
}
function handleExampleChange(e) {
const exampleId = e.target.value;
if (!exampleId) return;
const example = getExample(exampleId);
if (!example) {
console.error(`Example ${exampleId} not found`);
return;
}
document.getElementById('file-type').value = example.fileType;
state.currentFileType = example.fileType;
Editor.setEditorContent(example.code);
if (state.autoParse) {
handleParse();
}
e.target.value = '';
console.log(`📝 Loaded example: ${example.name}`);
}
async function handleParse() {
if (!state.isInitialized) {
showError('Playground not initialized yet');
return;
}
const code = Editor.getEditorContent();
if (!code || code.trim().length === 0) {
return;
}
try {
console.log(`🔍 Parsing ${state.currentFileType}...`);
const result = await Parser.parseCode(code, state.currentFileType);
state.lastParseResult = result;
Renderer.renderOutput(result);
TreeViz.renderTree(result.cst);
console.log(`✅ Parse complete in ${result.parseTimeMs.toFixed(2)}ms`);
} catch (error) {
console.error('❌ Parse failed:', error);
showError(`Parse failed: ${error.message}`);
}
}
function handleEditorChange() {
if (state.autoParse) {
debouncedParse();
}
saveToLocalStorage();
}
function debouncedParse() {
if (state.parseTimeout) {
clearTimeout(state.parseTimeout);
}
state.parseTimeout = setTimeout(() => {
handleParse();
}, 500);
}
function handleAutoParseToggle(e) {
state.autoParse = e.target.checked;
console.log(`🔄 Auto-parse ${state.autoParse ? 'enabled' : 'disabled'}`);
}
function handleShare() {
console.log('🔧 TODO: Implement share functionality');
showError('Share functionality coming soon!');
}
function handleClear() {
if (confirm('Clear editor and output?')) {
Editor.clearEditor();
Renderer.clearOutput();
TreeViz.clearTree();
state.lastParseResult = null;
console.log('🗑️ Cleared editor and output');
}
}
function handleTabChange(tabId) {
state.activeTab = tabId;
document.querySelectorAll('.tab-btn').forEach(btn => {
btn.classList.toggle('active', btn.dataset.tab === tabId);
});
document.querySelectorAll('.tab-pane').forEach(pane => {
pane.classList.toggle('active', pane.id === `${tabId}-tab`);
});
console.log(`📑 Switched to ${tabId} tab`);
if (tabId === 'tree' && state.lastParseResult) {
TreeViz.renderTree(state.lastParseResult.cst);
}
}
function handleTokenFilter(e) {
console.log('🔧 TODO: Implement token filter:', e.target.value);
}
function handleTokenSearch(e) {
console.log('🔧 TODO: Implement token search:', e.target.value);
}
function handleExpandAll() {
document.querySelectorAll('.cst-node.collapsed').forEach(node => {
node.classList.remove('collapsed');
});
}
function handleCollapseAll() {
document.querySelectorAll('.cst-node').forEach(node => {
if (node.querySelector('.cst-node-children')) {
node.classList.add('collapsed');
}
});
}
function handleHighlightRequest(e) {
const { line, column, length } = e.detail;
Editor.highlightRange(line, column, line, column + length);
}
function handleHighlightNode(e) {
const { startOffset, endOffset } = e.detail;
const startPos = Editor.byteOffsetToPosition(startOffset);
const endPos = Editor.byteOffsetToPosition(endOffset);
Editor.highlightRange(
startPos.lineNumber,
startPos.column,
endPos.lineNumber,
endPos.column
);
}
function handleHighlightAndPosition(e) {
const { startOffset, endOffset } = e.detail;
const startPos = Editor.byteOffsetToPosition(startOffset);
const endPos = Editor.byteOffsetToPosition(endOffset);
Editor.highlightRange(
startPos.lineNumber,
startPos.column,
endPos.lineNumber,
endPos.column
);
Editor.setCursorToPosition(startPos.lineNumber, startPos.column);
}
function handleClearHighlight() {
Editor.clearHighlight();
}
function handleEditorCursorChange(e) {
if (!state.lastParseResult) {
return;
}
const { lineNumber, column } = e.detail;
if (state.activeTab === 'tokens') {
const token = findTokenAtPosition(state.lastParseResult.tokens, lineNumber, column);
if (token) {
highlightTokenRow(token);
}
} else if (state.activeTab === 'cst') {
const byteOffset = positionToByteOffset(lineNumber, column);
if (byteOffset !== null && state.lastParseResult.cst) {
const node = findMostSpecificCstNode(state.lastParseResult.cst, byteOffset);
if (node) {
highlightCstNode(node);
}
}
}
}
function findTokenAtPosition(tokens, line, column) {
const tokensOnLine = tokens.filter(t => t.line === line);
for (const token of tokensOnLine) {
const tokenEnd = token.column + token.length;
if (column >= token.column && column < tokenEnd) {
return token;
}
}
return null;
}
function highlightTokenRow(token) {
document.querySelectorAll('.tokens-table tbody tr.highlighted').forEach(row => {
row.classList.remove('highlighted');
});
const rows = document.querySelectorAll('.tokens-table tbody tr');
for (const row of rows) {
const rowLine = parseInt(row.dataset.line);
const rowColumn = parseInt(row.dataset.column);
if (rowLine === token.line && rowColumn === token.column) {
row.classList.add('highlighted');
row.scrollIntoView({ behavior: 'smooth', block: 'center' });
break;
}
}
}
function positionToByteOffset(line, column) {
const content = Editor.getEditorContent();
if (!content) return null;
let currentLine = 1;
let currentColumn = 1;
let offset = 0;
for (let i = 0; i < content.length; i++) {
if (currentLine === line && currentColumn === column) {
return offset;
}
if (content[i] === '\n') {
currentLine++;
currentColumn = 1;
} else {
currentColumn++;
}
offset++;
}
if (currentLine === line && currentColumn === column) {
return offset;
}
return null;
}
function findMostSpecificCstNode(node, byteOffset) {
if (!node || !node.range) return null;
const [start, end] = node.range;
if (byteOffset < start || byteOffset >= end) {
return null;
}
if (node.children && node.children.length > 0) {
let bestMatch = null;
let bestMatchDepth = 0;
let bestMatchSize = end - start;
for (const child of node.children) {
const specificChild = findMostSpecificCstNode(child, byteOffset);
if (specificChild) {
const childSize = specificChild.range[1] - specificChild.range[0];
const childDepth = getNodeDepth(specificChild);
if (!bestMatch || childSize < bestMatchSize ||
(childSize === bestMatchSize && childDepth > bestMatchDepth)) {
bestMatch = specificChild;
bestMatchSize = childSize;
bestMatchDepth = childDepth;
}
}
}
if (bestMatch) {
return bestMatch;
}
}
return node;
}
function getNodeDepth(node) {
if (!node.children || node.children.length === 0) {
return 0;
}
return 1 + Math.max(...node.children.map(child => getNodeDepth(child)));
}
function highlightCstNode(node) {
if (!node) return;
document.querySelectorAll('.cst-node.highlighted').forEach(el => {
el.classList.remove('highlighted');
});
const nodeId = node._nodeId;
if (nodeId === undefined) return;
const cstNodes = document.querySelectorAll('.cst-node');
for (const cstNode of cstNodes) {
const domNodeId = parseInt(cstNode.dataset.nodeId);
if (domNodeId === nodeId) {
cstNode.classList.add('highlighted');
let parent = cstNode.parentElement;
while (parent) {
if (parent.classList.contains('cst-node') && parent.classList.contains('collapsed')) {
parent.classList.remove('collapsed');
}
parent = parent.parentElement;
}
cstNode.scrollIntoView({ behavior: 'smooth', block: 'center' });
break;
}
}
}
function handleThemeToggle() {
Editor.updateEditorTheme();
}
function setupResizer() {
const resizer = document.getElementById('resizer');
const leftPanel = document.querySelector('.editor-panel');
const rightPanel = document.querySelector('.output-panel');
if (!resizer || !leftPanel || !rightPanel) return;
let isResizing = false;
let startX = 0;
let startLeftWidth = 0;
resizer.addEventListener('mousedown', (e) => {
isResizing = true;
startX = e.clientX;
startLeftWidth = leftPanel.offsetWidth;
document.body.style.cursor = 'col-resize';
e.preventDefault();
});
document.addEventListener('mousemove', (e) => {
if (!isResizing) return;
const deltaX = e.clientX - startX;
const newLeftWidth = startLeftWidth + deltaX;
const minWidth = 300;
const maxWidth = window.innerWidth - 300 - 8;
if (newLeftWidth >= minWidth && newLeftWidth <= maxWidth) {
leftPanel.style.width = `${newLeftWidth}px`;
leftPanel.style.flex = 'none';
}
});
document.addEventListener('mouseup', () => {
if (isResizing) {
isResizing = false;
document.body.style.cursor = '';
}
});
}
function handleWindowResize() {
console.log('↔️ Window resized');
}
function showLoading(message = 'Loading...') {
const overlay = document.getElementById('loading-overlay');
if (overlay) {
overlay.querySelector('p').textContent = message;
overlay.classList.remove('hidden');
}
}
function hideLoading() {
const overlay = document.getElementById('loading-overlay');
if (overlay) {
overlay.classList.add('hidden');
}
}
function showError(message) {
const modal = document.getElementById('error-modal');
const messageEl = document.getElementById('error-message');
if (modal && messageEl) {
messageEl.textContent = message;
modal.classList.remove('hidden');
}
console.error('❌ Error:', message);
}
function hideError() {
const modal = document.getElementById('error-modal');
if (modal) {
modal.classList.add('hidden');
}
}
document.querySelector('.modal-close')?.addEventListener('click', hideError);
document.getElementById('error-modal')?.addEventListener('click', (e) => {
if (e.target.id === 'error-modal') {
hideError();
}
});
function saveToLocalStorage() {
try {
const code = Editor.getEditorContent();
localStorage.setItem('vb6parse-playground-code', code);
localStorage.setItem('vb6parse-playground-filetype', state.currentFileType);
localStorage.setItem('vb6parse-playground-autoparse', state.autoParse);
} catch (error) {
console.warn('Failed to save to localStorage:', error);
}
}
function loadFromLocalStorage() {
try {
const code = localStorage.getItem('vb6parse-playground-code');
const fileType = localStorage.getItem('vb6parse-playground-filetype');
const autoParse = localStorage.getItem('vb6parse-playground-autoparse');
if (code) {
Editor.setEditorContent(code);
}
if (fileType) {
state.currentFileType = fileType;
document.getElementById('file-type').value = fileType;
}
if (autoParse !== null) {
state.autoParse = autoParse === 'true';
document.getElementById('auto-parse').checked = state.autoParse;
}
console.log('📂 Loaded state from localStorage');
} catch (error) {
console.warn('Failed to load from localStorage:', error);
}
}
function loadFromUrl() {
const params = new URLSearchParams(window.location.search);
const encodedCode = params.get('code');
const fileType = params.get('type');
if (encodedCode) {
try {
const code = decodeURIComponent(atob(encodedCode));
Editor.setEditorContent(code);
console.log('🔗 Loaded code from URL');
} catch (error) {
console.error('Failed to decode URL code:', error);
}
}
if (fileType) {
state.currentFileType = fileType;
document.getElementById('file-type').value = fileType;
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
export default {
init,
state
};