From 237c45259ad43e9172f26ec79357bac0a46b5381 Mon Sep 17 00:00:00 2001 From: Gordon Smith Date: Fri, 5 Sep 2025 09:41:49 +0100 Subject: [PATCH] fix: improve runtime placeholder handling Fixes previous issues with update and remove Signed-off-by: Gordon Smith --- packages/observablehq-compiler/index-kit.html | 5 +- .../observablehq-compiler/src/compiler.ts | 4 +- packages/observablehq-compiler/src/index.ts | 2 +- .../observablehq-compiler/src/kit/compiler.ts | 65 +++++++++++------- .../observablehq-compiler/src/kit/index.ts | 1 - .../observablehq-compiler/src/kit/runtime.ts | 66 +++++++++++++----- .../tests/index-notebookkit.js | 22 +++--- .../tests/index-notebookkit.ts | 68 +++++++++---------- 8 files changed, 138 insertions(+), 95 deletions(-) diff --git a/packages/observablehq-compiler/index-kit.html b/packages/observablehq-compiler/index-kit.html index 3236282709..be1e2fe9a7 100644 --- a/packages/observablehq-compiler/index-kit.html +++ b/packages/observablehq-compiler/index-kit.html @@ -5,13 +5,12 @@ Home ObservableHQ Kit Preview - - + diff --git a/packages/observablehq-compiler/src/compiler.ts b/packages/observablehq-compiler/src/compiler.ts index 785c6a4450..f4e432c375 100644 --- a/packages/observablehq-compiler/src/compiler.ts +++ b/packages/observablehq-compiler/src/compiler.ts @@ -1,5 +1,5 @@ -import { type Notebook, type Definition, compile as compileKit, fixRelativeUrl, isRelativePath, obfuscatedImport } from "./kit/index.ts"; +import { type Notebook, type Definition, compileNotebook, fixRelativeUrl, isRelativePath, obfuscatedImport } from "./kit/index.ts"; import { ohq, splitModule } from "./observable-shim.ts"; import { parseCell, ParsedImportCell } from "./cst.ts"; import { Writer } from "./writer.ts"; @@ -330,7 +330,7 @@ export async function compile(notebookOrOjs: ohq.Notebook, options?: CompileOpti export async function compile(notebookOrOjs: string, options?: CompileOptions): Promise; export async function compile(notebookOrOjs: Notebook | ohq.Notebook | string, { baseUrl = ".", importMode = "precompiled" }: CompileOptions = {}) { if (isNotebookKit(notebookOrOjs)) { - return compileKit(notebookOrOjs); + return compileNotebook(notebookOrOjs); } else if (typeof notebookOrOjs === "string") { notebookOrOjs = ojs2notebook(notebookOrOjs); } diff --git a/packages/observablehq-compiler/src/index.ts b/packages/observablehq-compiler/src/index.ts index 6b00daf765..086d448a6e 100644 --- a/packages/observablehq-compiler/src/index.ts +++ b/packages/observablehq-compiler/src/index.ts @@ -2,5 +2,5 @@ export type { ohq } from "./observable-shim.ts"; export * from "./compiler.ts"; export { ojs2notebook, omd2notebook, omd2notebookKit, ojs2notebookKit, download } from "./util.ts"; -export { compile as compileKit, html2notebook, notebook2html } from "./kit/index.ts"; +export * from "./kit/index.ts"; export * from "./writer.ts"; diff --git a/packages/observablehq-compiler/src/kit/compiler.ts b/packages/observablehq-compiler/src/kit/compiler.ts index c9479c9963..8a59402113 100644 --- a/packages/observablehq-compiler/src/kit/compiler.ts +++ b/packages/observablehq-compiler/src/kit/compiler.ts @@ -1,5 +1,5 @@ -import { type Notebook, transpile } from "@observablehq/notebook-kit"; +import { type Notebook, type Cell, transpile } from "@observablehq/notebook-kit"; import { type Definition } from "@observablehq/notebook-kit/runtime"; import { constructFunction } from "./util.ts"; @@ -7,35 +7,50 @@ export interface CompileKitOptions { inline?: boolean; } -export function compile(notebook: Notebook, options: CompileKitOptions = { inline: true }): Definition[] { +export function compileCell(cell: Cell, options: CompileKitOptions = { inline: true }): Definition[] { const retVal: Definition[] = []; - let id = 1; - for (const cell of notebook.cells) { - try { - const compiled = transpile(cell); - retVal.push({ - id: id++, - ...compiled, - body: options.inline ? constructFunction(compiled.body, `cell_${id}`) : compiled.body, - }); - if (cell.pinned) { - const compiled = transpile({ - ...cell, - mode: "md", - value: `\ + const sourceIDOffset = 1000000; + try { + const compiled = transpile(cell); + retVal.push({ + id: cell.id, + ...compiled, + body: options.inline ? constructFunction(compiled.body, `cell_${cell.id}`) : compiled.body, + }); + if (cell.pinned) { + const compiled = transpile({ + ...cell, + mode: "md", + value: `\ \`\`\`${cell.mode} ${cell.value} \`\`\`` - }); - retVal.push({ - id: id++, - ...compiled, - body: options.inline ? constructFunction(compiled.body, `cell_${id}`) : compiled.body, - }); - } - } catch (error) { - console.error(`Error compiling cell ${id}:`, error); + }); + retVal.push({ + id: sourceIDOffset + cell.id, + ...compiled, + body: options.inline ? constructFunction(compiled.body, `cell_source_${sourceIDOffset + cell.id}`) : compiled.body, + }); } + } catch (error) { + console.error(`Error compiling cell ${cell.id}:`, error); + } + return retVal; +} + +export function compileNotebook(notebook: Notebook, options: CompileKitOptions = { inline: true }): Definition[] { + const retVal: Definition[] = []; + for (const cell of notebook.cells) { + const cellDefs = compileCell(cell, options); + retVal.push(...cellDefs); } return retVal; } + +export function resetCellIDs(notebook: Notebook, start: number = 0, increment: number = 1): Notebook { + for (const cell of notebook.cells) { + cell.id = start; + start += increment; + } + return notebook; +} diff --git a/packages/observablehq-compiler/src/kit/index.ts b/packages/observablehq-compiler/src/kit/index.ts index 6d5695c998..e88eec58a1 100644 --- a/packages/observablehq-compiler/src/kit/index.ts +++ b/packages/observablehq-compiler/src/kit/index.ts @@ -1,3 +1,2 @@ -export * from "./util.ts"; export * from "./compiler.ts"; export * from "./util.ts"; diff --git a/packages/observablehq-compiler/src/kit/runtime.ts b/packages/observablehq-compiler/src/kit/runtime.ts index fae26f24a6..b96c48089a 100644 --- a/packages/observablehq-compiler/src/kit/runtime.ts +++ b/packages/observablehq-compiler/src/kit/runtime.ts @@ -4,6 +4,8 @@ import { type Definition } from "./index.ts"; import "@observablehq/notebook-kit/index.css"; import "@observablehq/notebook-kit/theme-air.css"; +export { DefineState }; + export class NotebookRuntime extends NotebookRuntimeBase { stateById = new Map(); @@ -16,23 +18,50 @@ export class NotebookRuntime extends NotebookRuntimeBase { return this.stateById.has(cellId); } - async add(cellId: number, definition: Definition, placeholderDiv: HTMLDivElement): Promise { - let state: DefineState | undefined = this.stateById.get(cellId); - if (state) { - this.remove(cellId); + async add(definition: Definition): Promise { + if (this.stateById.has(definition.id)) { + throw new Error(`Cell with id ${definition.id} already exists`); } - state = { root: placeholderDiv, expanded: [], variables: [] }; - this.stateById.set(cellId, state); + const placeholderDiv = document.createElement("div"); + placeholderDiv.className = "observablehq observablehq--cell"; + const state = { root: placeholderDiv, expanded: [], variables: [] }; + this.stateById.set(definition.id, state); this.define(state, definition); await this.runtime._computeNow(); + return state.root; } - async remove(cellId: number): Promise { + async update(definition: Definition): Promise { + const state = this.stateById.get(definition.id); + if (!state) { + throw new Error(`Cell with id ${definition.id} does not exist`); + } + await this.clear(definition.id); + this.define(state, definition); + await this.runtime._computeNow(); + return state.root; + } + + async clear(cellId: number): Promise { const state = this.stateById.get(cellId); - if (!state) return; + if (!state) { + throw new Error(`Cell with id ${cellId} does not exist`); + } [...state.variables].forEach(v => v.delete()); + await this.runtime._computeNow(); + state.variables = []; + state.expanded = []; + state.root.innerHTML = ""; + } + + async remove(cellId: number): Promise { + const state = this.stateById.get(cellId); + if (!state) { + throw new Error(`Cell with id ${cellId} does not exist`); + } + void this.clear(cellId); this.stateById.delete(cellId); - state.root?.remove(); + state.root.remove(); await this.runtime._computeNow(); } @@ -44,14 +73,15 @@ export class NotebookRuntime extends NotebookRuntimeBase { await this.runtime._computeNow(); } - render(definitions: Definition[], target: HTMLElement) { - definitions.forEach(definition => { - const placeholderDiv = document.createElement("div"); - placeholderDiv.id = `cell-${definition.id}`; - placeholderDiv.className = "observablehq observablehq--cell"; - this.stateById.set(definition.id, { root: placeholderDiv, expanded: [], variables: [] }); - this.define(this.stateById.get(definition.id)!, definition); - target.appendChild(placeholderDiv); - }); + async render(definitions: Definition[], target: HTMLElement) { + for (const definition of definitions) { + let observableDiv: HTMLDivElement; + if (this.stateById.has(definition.id)) { + observableDiv = await this.update(definition); + } else { + observableDiv = await this.add(definition); + } + target.appendChild(observableDiv); + } } } diff --git a/packages/observablehq-compiler/tests/index-notebookkit.js b/packages/observablehq-compiler/tests/index-notebookkit.js index e1e88fcfd4..85e6e3c85c 100644 --- a/packages/observablehq-compiler/tests/index-notebookkit.js +++ b/packages/observablehq-compiler/tests/index-notebookkit.js @@ -1,4 +1,4 @@ -import { compileKit, html2notebook } from "@hpcc-js/observablehq-compiler"; +import { compileNotebook, html2notebook } from "@hpcc-js/observablehq-compiler"; import { NotebookRuntime } from "@hpcc-js/observablehq-compiler/runtime"; // import "@observablehq/notebook-kit/theme-air.css"; @@ -19,13 +19,13 @@ import { NotebookRuntime } from "@hpcc-js/observablehq-compiler/runtime"; export async function testHtml(target) { - const element = document.getElementById(target); - if (!element) { - throw new Error(`Element with id ${target} not found`); - } + const element = document.getElementById(target); + if (!element) { + throw new Error(`Element with id ${target} not found`); + } - // const html = await fetch("../tests/albers-usa-projection.txt"); - const html = `\ + // const html = await fetch("../tests/albers-usa-projection.txt"); + const html = `\ Observable Notebooks 2.0 System guide @@ -210,8 +210,8 @@ export async function testHtml(target) { `; - const notebook = html2notebook(html); - const definitions = compileKit(notebook); - const runtime = new NotebookRuntime(); - runtime.render(definitions, element); + const notebook = html2notebook(html); + const definitions = compileNotebook(notebook); + const runtime = new NotebookRuntime(); + runtime.render(definitions, element); } diff --git a/packages/observablehq-compiler/tests/index-notebookkit.ts b/packages/observablehq-compiler/tests/index-notebookkit.ts index 3c90299e86..4efb8b6292 100644 --- a/packages/observablehq-compiler/tests/index-notebookkit.ts +++ b/packages/observablehq-compiler/tests/index-notebookkit.ts @@ -1,4 +1,4 @@ -import { omd2notebookKit, ojs2notebookKit, compileKit, html2notebook } from "../src/index.ts"; +import { omd2notebookKit, ojs2notebookKit, compileNotebook, html2notebook } from "../src/index.ts"; import { NotebookRuntime } from "../src/kit/runtime.ts"; // import "@observablehq/notebook-kit/theme-air.css"; @@ -6,7 +6,7 @@ import { NotebookRuntime } from "../src/kit/runtime.ts"; // import "@observablehq/notebook-kit/theme-cotton.css"; // import "@observablehq/notebook-kit/theme-deep-space.css"; // import "@observablehq/notebook-kit/theme-glacier.css"; -// import "@observablehq/notebook-kit/theme-ink.css"; +import "@observablehq/notebook-kit/theme-ink.css"; // import "@observablehq/notebook-kit/theme-midnight.css"; // import "@observablehq/notebook-kit/theme-near-midnight.css"; // import "@observablehq/notebook-kit/theme-ocean-floor.css"; @@ -19,28 +19,28 @@ import { NotebookRuntime } from "../src/kit/runtime.ts"; export async function testHtml(target: string): Promise { - const element = document.getElementById(target) as HTMLDivElement; - if (!element) { - throw new Error(`Element with id ${target} not found`); - } - - // const html = await fetch("../tests/albers-usa-projection.txt"); - const html = await fetch("../tests/system-guide.txt") - .then((response) => { - return response.text(); - }); - const notebook = html2notebook(html); - const definitions = compileKit(notebook); - const runtime = new NotebookRuntime(); - runtime.render(definitions, element); + const element = document.getElementById(target) as HTMLDivElement; + if (!element) { + throw new Error(`Element with id ${target} not found`); + } + + // const html = await fetch("../tests/albers-usa-projection.txt"); + const html = await fetch("../tests/system-guide.txt") + .then((response) => { + return response.text(); + }); + const notebook = html2notebook(html); + const definitions = compileNotebook(notebook); + const runtime = new NotebookRuntime(); + runtime.render(definitions, element); } export function testOmd(target: string): void { - const element = document.getElementById(target) as HTMLDivElement; - if (!element) { - throw new Error(`Element with id ${target} not found`); - } - const omd2 = `\ + const element = document.getElementById(target) as HTMLDivElement; + if (!element) { + throw new Error(`Element with id ${target} not found`); + } + const omd2 = `\ ## Imports (dot) @@ -331,18 +331,18 @@ value `; - const notebook = omd2notebookKit(omd2); - const definitions = compileKit(notebook); - const runtime = new NotebookRuntime(); - runtime.render(definitions, element); + const notebook = omd2notebookKit(omd2); + const definitions = compileKit(notebook); + const runtime = new NotebookRuntime(); + runtime.render(definitions, element); } export function testOjs(target: string): void { - const element = document.getElementById(target) as HTMLDivElement; - if (!element) { - throw new Error(`Element with id ${target} not found`); - } - const ojs = `\ + const element = document.getElementById(target) as HTMLDivElement; + if (!element) { + throw new Error(`Element with id ${target} not found`); + } + const ojs = `\ 2 * 3 * 7 { let sum = 0; @@ -357,8 +357,8 @@ md \`# lib.ojs\`; mo = 38 + 40; `; - const notebook = ojs2notebookKit(ojs); - const definitions = compileKit(notebook); - const runtime = new NotebookRuntime(); - runtime.render(definitions, element); + const notebook = ojs2notebookKit(ojs); + const definitions = compileKit(notebook); + const runtime = new NotebookRuntime(); + runtime.render(definitions, element); } \ No newline at end of file