Skip to content

Commit b8a358d

Browse files
committed
feat: Add @observablehq/notebook-kit support
Signed-off-by: Gordon Smith <[email protected]>
1 parent 49ba456 commit b8a358d

16 files changed

+3262
-1449
lines changed

package-lock.json

Lines changed: 1934 additions & 1409 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/observablehq-compiler/.vscode/launch.json

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,34 @@
5656
"${workspaceFolder}/**/*.js",
5757
"!**/node_modules/**"
5858
]
59+
},
60+
{
61+
"name": "index-kit.html",
62+
"request": "launch",
63+
"type": "msedge",
64+
"url": "http://localhost:5514/index-kit.html",
65+
"runtimeArgs": [
66+
"--disable-web-security"
67+
],
68+
"webRoot": "${workspaceFolder}",
69+
"outFiles": [
70+
"${workspaceFolder}/**/*.js",
71+
"!**/node_modules/**"
72+
]
73+
},
74+
{
75+
"name": "index-kit-preview.html",
76+
"request": "launch",
77+
"type": "msedge",
78+
"url": "file://${workspaceFolder}/index-kit-preview.html",
79+
"runtimeArgs": [
80+
"--disable-web-security"
81+
],
82+
"webRoot": "${workspaceFolder}",
83+
"outFiles": [
84+
"${workspaceFolder}/**/*.js",
85+
"!**/node_modules/**"
86+
]
5987
}
6088
]
6189
}
Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
import { neutralTpl } from "@hpcc-js/esbuild-plugins";
1+
import { nodeBoth } from "@hpcc-js/esbuild-plugins";
22

33
// config ---
4-
await neutralTpl("src/index.ts", "dist/index");
4+
await Promise.all([
5+
nodeBoth("src/index.node.ts", "dist/node/index")
6+
]);
Lines changed: 258 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,258 @@
1+
<!DOCTYPE html>
2+
<html>
3+
4+
<head>
5+
<title>Home</title>
6+
<style>
7+
body {
8+
font-family: Arial, sans-serif;
9+
margin: 0;
10+
padding: 0;
11+
background-color: #f0f0f0;
12+
}
13+
14+
h1 {
15+
text-align: center;
16+
margin-top: 50px;
17+
}
18+
19+
#placeholder {
20+
width: 100%;
21+
height: 500px;
22+
background-color: #fff;
23+
margin-top: 20px;
24+
}
25+
</style>
26+
<script type="importmap">
27+
{
28+
"imports": {
29+
"@observablehq/notebook-kit/runtime": "https://cdn.jsdelivr.net/npm/@observablehq/[email protected]/dist/src/index.js",
30+
"@observablehq/notebook-kit/index.css": "https://cdn.jsdelivr.net/npm/@observablehq/[email protected]/dist/src/styles/index.css",
31+
"acorn": "https://cdn.jsdelivr.net/npm/[email protected]/dist/acorn.js",
32+
"@observablehq/parser": "https://cdn.jsdelivr.net/npm/@observablehq/[email protected]/dist/parser.min.js",
33+
"acorn-walk": "https://cdn.jsdelivr.net/npm/[email protected]/dist/walk.mjs",
34+
35+
"@hpcc-js/observablehq-compiler": "../observablehq-compiler/dist/index.js"
36+
}
37+
}
38+
</script>
39+
</head>
40+
41+
<body>
42+
<h1>ESM Quick Test</h1>
43+
<div id="placeholder"></div>
44+
<script type="module">
45+
// import { NotebookRuntime } from "@observablehq/notebook-kit/runtime";
46+
import { kit, omd2notebookKit, ojs2notebookKit, NotebookRuntime } from "@hpcc-js/observablehq-compiler";
47+
48+
class NotebookRuntimeEx extends NotebookRuntime {
49+
50+
stateById = new Map();
51+
52+
constructor() {
53+
super();
54+
}
55+
56+
add(cellId , definition, placeholderDiv) {
57+
let state = this.stateById.get(cellId);
58+
if (state) {
59+
state.variables.forEach((v) => v.delete());
60+
state.variables = [];
61+
} else {
62+
state = { root: placeholderDiv, expanded: [], variables: [] };
63+
this.stateById.set(cellId, state);
64+
}
65+
this.define(state, definition);
66+
}
67+
68+
remove(cellId) {
69+
const state = this.stateById.get(cellId);
70+
if (!state) return;
71+
state.root.remove();
72+
state.variables.forEach((v) => v.delete());
73+
this.stateById.delete(cellId);
74+
}
75+
76+
removeAll() {
77+
const keys = Array.from(this.stateById.keys());
78+
for (const key of keys) {
79+
this.remove(key);
80+
}
81+
}
82+
}
83+
84+
const target = "placeholder";
85+
const element = document.getElementById(target);
86+
if (!element) {
87+
throw new Error(`Element with id ${target} not found`);
88+
}
89+
90+
const omd = `\
91+
# Five-Minute Introduction
92+
93+
\`\`\`ecl
94+
r := RECORD
95+
STRING20 Subject;
96+
INTEGER4 Result;
97+
END;
98+
99+
d := DATASET([
100+
{'English', 92},
101+
{'French', 86},
102+
{'Irish', 80},
103+
{'Math', 98},
104+
{'Geography', 55},
105+
{'Computers', 25}], r);
106+
OUTPUT(d, {Label := Subject, Value := Result}, NAMED('BarChartData'));
107+
\`\`\`
108+
109+
\`\`\`
110+
viewof colorzzz = html\`<input type="color" value="#0000ff">\`
111+
html\`The color input (viewof colorzzz) is a \${viewof colorzzz.constructor.name}.\`
112+
viewof colorzzz;
113+
\`\`\`
114+
115+
Welcome! This notebook gives a quick overview of "Observable Markdown" a mashup of the excellent [Observable HQ](https://observablehq.com) + regular Markdown. Here follows a quick introduction to Observable. For a more technical introduction, see [Observable’s not JavaScript](/@observablehq/observables-not-javascript). For hands-on, see our [introductory tutorial series](/collection/@observablehq/introduction). To watch rather than read, see our [short introductory video](https://www.youtube.com/watch?v=uEmDwflQ3xE)!
116+
117+
Its also very easy to embed a value: \${i} inside the Markdown!!!
118+
119+
Observable Markdown consists of a single markdown document with live "code" sections.
120+
121+
\`\`\`
122+
2 * 3 * 7
123+
{
124+
let sum = 0;
125+
for (let i = 0; i <= 100; ++i) {
126+
sum += i;
127+
}
128+
return sum;
129+
}
130+
\`\`\`
131+
132+
Cells can have names. This allows a cell’s value to be referenced by other cells.
133+
134+
\`\`\`
135+
color = "red";
136+
\`My favorite color is \${color}.\`
137+
\`\`\`
138+
139+
A cell referencing another cell is re-evaluated automatically when the referenced value changes. Try editing the definition of color above and shift-return to re-evaluate.
140+
141+
Cells can generate DOM (HTML, SVG, Canvas, WebGL, etc.). You can use the standard DOM API like document.createElement, or use the built-in html tagged template literal:
142+
143+
\`\`\`
144+
html\`<span style="background:yellow;">
145+
My favorite language is <i>HTML</i>.
146+
</span>\`
147+
\`\`\`
148+
149+
There’s a Markdown tagged template literal, too. (This notebook is written in Markdown.)
150+
151+
\`\`\`
152+
md\`My favorite language is *Markdown*.\`
153+
\`\`\`
154+
155+
DOM can be made reactive simply by referring to other cells. The next cell refers to color. (Try editing the definition of color above.)
156+
157+
\`\`\`
158+
html\`My favorite color is <i style="background:\${color};">\${color}</i>.\`
159+
\`\`\`
160+
161+
Sometimes you need to load data from a remote server, or compute something expensive in a web worker. For that, cells can be defined asynchronously using [promises](https://developer.mozilla.org/docs/Web/JavaScript/Guide/Using_promises):
162+
163+
\`\`\`
164+
status = new Promise(resolve => {
165+
setTimeout(() => {
166+
resolve({resolved: new Date});
167+
}, 2000);
168+
})
169+
\`\`\`
170+
171+
A cell that refers to a promise cell sees the value when it is resolved; this implicit await means that referencing cells don’t care whether the value is synchronous or not. Edit the status cell above to see the cell below update after two seconds.
172+
173+
\`\`\`
174+
status
175+
\`\`\`
176+
177+
Promises are also useful for loading libraries from npm. Below, require returns a promise that resolves to the d3-fetch library:
178+
179+
\`\`\`
180+
d3 = require("d3-fetch@1")
181+
\`\`\`
182+
183+
If you prefer, you can use async and await explicitly (not this ):
184+
185+
\`\`\`
186+
countries = (await d3.tsv("https://cdn.jsdelivr.net/npm/world-atlas@1/world/110m.tsv"))
187+
.sort((a, b) => b.pop_est - a.pop_est) // Sort by descending estimated population.
188+
.slice(0, 10) // Take the top ten.
189+
\`\`\`
190+
191+
Cells can be defined as [generators](https://developer.mozilla.org/docs/Web/JavaScript/Guide/Iterators_and_Generators#Generators); a value is yielded up to sixty times a second.
192+
193+
\`\`\`
194+
i = {
195+
let i = 0;
196+
while (true) {
197+
yield ++i;
198+
}
199+
}
200+
\`The current value of i is \${i}.\`
201+
\`\`\`
202+
203+
Any cell that refers to a generator cell sees its current value; the referencing cell is re-evaluated whenever the generator yields a new value. As you might guess, a generator can yield promises for [async iteration](https://github.com/tc39/proposal-async-iteration); referencing cells see the current resolved value.
204+
205+
\`\`\`
206+
date = {
207+
while (true) {
208+
yield new Promise(resolve => {
209+
setTimeout(() => resolve(new Date), 1000);
210+
});
211+
}
212+
}
213+
\`\`\`
214+
215+
Combining these primitives—promises, generators and DOM—you can build custom user interfaces. Here’s a slider and a generator that yields the slider’s value:
216+
217+
\`\`\`
218+
slider = html\`<input type=range>\`
219+
sliderValue = Generators.input(slider)
220+
\`\`\`
221+
222+
Generators.input returns a generator that yields promises. The promise resolves whenever the associated input element emits an input event. You don’t need to implement that generator by hand, though. There’s a builtin viewof operator which exposes the current value of a given input element:
223+
224+
\`\`\`
225+
viewof value = html\`<input type=range>\`
226+
value
227+
\`\`\`
228+
229+
## Imports (dot)
230+
231+
\`\`\`
232+
dot\`digraph { x -> y -> z; }\`;
233+
\`\`\`
234+
235+
\`\`\`
236+
import { dot } from "@gordonsmith/graphviz";
237+
\`\`\`
238+
239+
`;
240+
241+
const runtime = new NotebookRuntimeEx();
242+
const notebook = omd2notebookKit(omd);
243+
const compiled = kit.compile(notebook);
244+
compiled.forEach((cell, cellId) => {
245+
let container = element.querySelector(`#${target}-${cellId}`);
246+
if (!container) {
247+
container = document.createElement("div");
248+
container.className = "observable-kit-cell-output";
249+
container.id = `${target}-${cellId}`;
250+
element.appendChild(container);
251+
}
252+
runtime.add(cellId, cell, container);
253+
});
254+
255+
</script>
256+
</body>
257+
258+
</html>
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<!DOCTYPE html>
2+
<html>
3+
4+
<head>
5+
<title>Home</title>
6+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
7+
<style>
8+
</style>
9+
</head>
10+
11+
<body>
12+
<h1>ESM Quick Test</h1>
13+
<main class="placeholder" id="placeholder-kit"></main>
14+
<script type="module">
15+
import { testHtml } from "./tests/index-notebookkit.ts";
16+
testHtml("placeholder-kit");
17+
</script>
18+
<hr>
19+
<div class="placeholder" id="placeholder-ojs"></div>
20+
<script type="module">
21+
// import { testOjs } from "./tests/index-notebookkit.ts";
22+
// testOjs("placeholder-ojs");
23+
</script>
24+
<hr>
25+
<div class="placeholder" id="placeholder-omd"></div>
26+
<script type="module">
27+
// import { testOmd } from "./tests/index-notebookkit.ts";
28+
// testOmd("placeholder-omd");
29+
</script>
30+
31+
</body>
32+
33+
</html>

packages/observablehq-compiler/package.json

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,9 @@
2525
},
2626
"scripts": {
2727
"clean": "rimraf --glob lib* types dist *.tsbuildinfo .turbo",
28-
"bundle": "vite build",
28+
"bundle-node": "node ./esbuild.js",
29+
"bundle-browser": "vite build",
30+
"bundle": "run-s bundle-browser bundle-node",
2931
"bundle-watch": "vite --port 5514",
3032
"gen-types": "tsc --project tsconfig.json",
3133
"gen-types-watch": "npm run gen-types -- --watch",
@@ -43,12 +45,16 @@
4345
"update-major": "npx --yes npm-check-updates -u"
4446
},
4547
"dependencies": {
48+
"jsdom": "26.1.0",
4649
"yargs": "17.7.2"
4750
},
4851
"devDependencies": {
4952
"@hpcc-js/esbuild-plugins": "^1.4.9",
53+
"@observablehq/notebook-kit": "1.1.0-rc.16",
5054
"@observablehq/parser": "6.1.0",
51-
"@observablehq/runtime": "5.9.9"
55+
"@observablehq/runtime": "5.9.9",
56+
"@types/jsdom": "21.1.7",
57+
"vite-plugin-top-level-await": "^1.6.0"
5258
},
5359
"repository": {
5460
"type": "git",

0 commit comments

Comments
 (0)