Enter a query to visualize
'; + return; + } + + try { + setStatus("Executing query...", "loading"); + + if (contextManager.hasVisual(query)) { + const result = contextManager.execute(query); + const spec = JSON.parse(result); + + vizOutput.innerHTML = ""; + + const warnings: string[] = []; + let _level = Warn; + const logger = { + level(_: number) { if (arguments.length) { _level = _; return this; } return _level; }, + error: (...args: any[]) => { console.error(...args); return logger; }, + warn: (...args: any[]) => { warnings.push(args.map(String).join(" ")); return logger; }, + info: () => logger, + debug: () => logger, + }; + + await vegaEmbed(vizOutput, spec, { + actions: { + export: true, + source: false, + compiled: false, + editor: false, + }, + renderer: "svg", + logger: logger as any, + }); + + showProblems([], warnings); + } else { + const result = JSON.parse(contextManager.executeSql(query)); + vizOutput.innerHTML = renderTable(result); + showProblems([], []); + } + + setStatus("Query executed successfully", "success"); + } catch (error: any) { + console.error("Query execution error:", error); + showProblems([error.toString()], []); + setStatus("Query error", "error"); + } +} + +// File upload handlers +csvUpload.addEventListener("change", async (e) => { + const file = (e.target as HTMLInputElement).files?.[0]; + if (!file) return; + + try { + setStatus("Uploading data...", "loading"); + await tableManager.uploadFile(file); + setStatus("Uploaded: " + file.name, "success"); + csvUpload.value = ""; + } catch (error: any) { + showProblems(["Upload failed: " + error], []); + setStatus("Upload error", "error"); + } +}); + +function initializeExamples() { + let currentSection = ""; + examples.forEach((example) => { + if (example.section !== currentSection) { + currentSection = example.section; + const header = document.createElement("div"); + header.className = "example-section-header"; + header.textContent = currentSection; + examplesList.appendChild(header); + } + const button = document.createElement("button"); + button.className = "example-button"; + button.textContent = example.name; + button.onclick = () => { + editorManager.setValue(example.query); + //executeQuery(example.query); + }; + examplesList.appendChild(button); + }); +} + +function initializeMobileExamples() { + const select = document.getElementById( + "mobile-example-select", + ) as HTMLSelectElement; + + let currentSection = ""; + let optgroup: HTMLOptGroupElement | null = null; + examples.forEach((example, index) => { + if (example.section !== currentSection) { + currentSection = example.section; + optgroup = document.createElement("optgroup"); + optgroup.label = currentSection; + select.appendChild(optgroup); + } + const option = document.createElement("option"); + option.value = String(index); + option.textContent = example.name; + optgroup!.appendChild(option); + }); + + select.addEventListener("change", () => { + const idx = parseInt(select.value, 10); + if (!isNaN(idx) && examples[idx]) { + editorManager.setValue(examples[idx].query); + } + }); +} + +async function main() { + try { + setStatus("Loading WASM module...", "loading"); + await contextManager.initialize(); + + // Load builtin datasets + setStatus("Loading builtin datasets...", "loading"); + await contextManager.registerBuiltinDatasets(); + + setStatus("Initializing editor...", "loading"); + await editorManager.initialize(editorContainer, examples[0].query); + + tableManager = new TableManager(tableList, contextManager); + tableManager.onClickTable((name) => { + editorManager.setValue(`SELECT * FROM ${name}`); + }); + tableManager.refresh(); + + initializeExamples(); + initializeMobileExamples(); + + editorManager.onChange((query) => { + executeQuery(query); + }); + + setStatus("Ready", "success"); + + executeQuery(examples[0].query); + } catch (error: any) { + console.error("Initialization error:", error); + setStatus("Initialization failed", "error"); + showProblems(["Failed to initialize: " + error], []); + } +} + +main(); diff --git a/ggsql-wasm/demo/src/quarto/editor.ts b/ggsql-wasm/demo/src/quarto/editor.ts new file mode 100644 index 00000000..61f89386 --- /dev/null +++ b/ggsql-wasm/demo/src/quarto/editor.ts @@ -0,0 +1,213 @@ +import * as monaco from "monaco-editor"; +import { + createOnigScanner, + createOnigString, + loadWASM, +} from "vscode-oniguruma"; +import { Registry, parseRawGrammar, type IGrammar } from "vscode-textmate"; +import { WASM_BASE } from "../wasmBase"; + +// Must be set before any Monaco editor is created +(self as any).MonacoEnvironment = { + getWorkerUrl: (_moduleId: string, _label: string) => + WASM_BASE + "editor.worker.js", +}; + +// Map TextMate scope names to Monaco theme token colors +const SCOPE_TO_TOKEN: [string, string][] = [ + ["comment", "comment"], + ["string", "string"], + ["constant.numeric", "number"], + ["constant.language", "keyword"], + ["keyword", "keyword"], + ["support.function", "type"], + ["support.type.geom", "type"], + ["support.type.aesthetic", "variable"], + ["support.type.coord", "type"], + ["support.type.theme", "type"], + ["support.type.property", "variable"], + ["constant.language.scale-type", "type"], + ["keyword.operator", "operator"], + ["punctuation", "delimiter"], +]; + +function scopeToMonacoToken(scopes: string[]): string { + for (let i = scopes.length - 1; i >= 0; i--) { + const scope = scopes[i]; + for (const [pattern, token] of SCOPE_TO_TOKEN) { + if (scope.startsWith(pattern)) { + return token; + } + } + } + return ""; +} + +// Singleton grammar initialization +let grammarPromise: Promise