@observablehq/notebook-kit 1.0.1 → 1.1.0-rc.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/build.js +7 -0
- package/dist/bin/download.js +15 -19
- package/dist/package.json +31 -2
- package/dist/src/databases/duckdb.d.ts +2 -0
- package/dist/src/databases/duckdb.js +72 -0
- package/dist/src/databases/index.d.ts +46 -0
- package/dist/src/databases/index.js +54 -0
- package/dist/src/databases/options.d.ts +3 -0
- package/dist/src/databases/options.js +6 -0
- package/dist/src/databases/postgres.d.ts +2 -0
- package/dist/src/databases/postgres.js +95 -0
- package/dist/src/databases/snowflake.d.ts +2 -0
- package/dist/src/databases/snowflake.js +102 -0
- package/dist/src/databases/sqlite.d.ts +2 -0
- package/dist/src/databases/sqlite.js +64 -0
- package/dist/src/javascript/imports/npm.js +2 -0
- package/dist/src/javascript/observable.js +24 -3
- package/dist/src/javascript/template.d.ts +3 -0
- package/dist/src/javascript/template.js +17 -1
- package/dist/src/javascript/transpile.d.ts +4 -2
- package/dist/src/javascript/transpile.js +30 -11
- package/dist/src/javascript/transpile.test.js +16 -0
- package/dist/src/lib/error.d.ts +6 -0
- package/dist/src/lib/error.js +6 -0
- package/dist/src/lib/hash.d.ts +2 -0
- package/dist/src/lib/hash.js +20 -0
- package/dist/src/lib/hash.test.d.ts +1 -0
- package/dist/src/lib/hash.test.js +28 -0
- package/dist/src/lib/notebook.d.ts +11 -1
- package/dist/src/lib/notebook.js +10 -3
- package/dist/src/lib/notebook.test.js +10 -2
- package/dist/src/lib/serialize.d.ts +3 -1
- package/dist/src/lib/serialize.js +13 -4
- package/dist/src/lib/serialize.test.js +10 -3
- package/dist/src/lib/sluggify.d.ts +6 -0
- package/dist/src/lib/sluggify.js +22 -0
- package/dist/src/lib/sluggify.test.d.ts +1 -0
- package/dist/src/lib/sluggify.test.js +51 -0
- package/dist/src/runtime/define.d.ts +2 -2
- package/dist/src/runtime/define.js +2 -2
- package/dist/src/runtime/display.d.ts +2 -0
- package/dist/src/runtime/display.js +5 -1
- package/dist/src/runtime/index.d.ts +100 -1
- package/dist/src/runtime/index.js +29 -3
- package/dist/src/runtime/stdlib/databaseClient.d.ts +45 -0
- package/dist/src/runtime/stdlib/databaseClient.js +77 -0
- package/dist/src/runtime/stdlib/duckdb.js +7 -4
- package/dist/src/runtime/stdlib/fileAttachment.d.ts +38 -11
- package/dist/src/runtime/stdlib/fileAttachment.js +21 -10
- package/dist/src/runtime/stdlib/generators/input.d.ts +1 -1
- package/dist/src/runtime/stdlib/index.d.ts +43 -6
- package/dist/src/runtime/stdlib/index.js +5 -5
- package/dist/src/styles/global.css +1 -1
- package/dist/src/templates/default.html +18 -18
- package/dist/src/vite/config.js +5 -2
- package/dist/src/vite/observable.d.ts +25 -6
- package/dist/src/vite/observable.js +42 -15
- package/package.json +31 -2
- package/dist/src/runtime/stdlib/sql.d.ts +0 -5
- package/dist/src/runtime/stdlib/sql.js +0 -5
|
@@ -10,13 +10,13 @@ export function transpileObservable(input, options) {
|
|
|
10
10
|
if (cell.tag)
|
|
11
11
|
throw new Error("tagged ojs cells are not supported");
|
|
12
12
|
const output = new Sourcemap(input).trim();
|
|
13
|
+
rewriteSpecialReferences(output, cell.body);
|
|
13
14
|
if (cell.body.type === "ImportDeclaration") {
|
|
14
15
|
rewriteImportSource(output, cell.body);
|
|
15
16
|
return transpileJavaScript(String(output));
|
|
16
17
|
}
|
|
17
18
|
if (options?.resolveFiles)
|
|
18
19
|
rewriteFileExpressions(output, cell.body);
|
|
19
|
-
rewriteSpecialReferences(output, cell.body);
|
|
20
20
|
const inputs = Array.from(new Set(cell.references.map(asReference)));
|
|
21
21
|
let start = "";
|
|
22
22
|
let end = "";
|
|
@@ -39,13 +39,15 @@ export function transpileObservable(input, options) {
|
|
|
39
39
|
autoview: cell.id?.type === "ViewExpression"
|
|
40
40
|
};
|
|
41
41
|
}
|
|
42
|
+
/** Rewrite bare module specifiers to have the observable: protocol. */
|
|
42
43
|
function rewriteImportSource(output, body) {
|
|
43
44
|
const specifier = body.source.value;
|
|
44
|
-
if (typeof specifier === "string" && !/^\w+:/.test(specifier))
|
|
45
|
+
if (typeof specifier === "string" && !/^\w+:/.test(specifier)) {
|
|
45
46
|
output.insertLeft(body.source.start + 1, "observable:");
|
|
47
|
+
}
|
|
46
48
|
output.insertRight(body.end, ";");
|
|
47
49
|
}
|
|
48
|
-
|
|
50
|
+
/** Rewrite viewof x ↦ viewof$x, and mutable x ↦ mutable$x.value. */
|
|
49
51
|
function rewriteSpecialReferences(output, body) {
|
|
50
52
|
simple(body, {
|
|
51
53
|
MutableExpression(node) {
|
|
@@ -53,9 +55,28 @@ function rewriteSpecialReferences(output, body) {
|
|
|
53
55
|
},
|
|
54
56
|
ViewExpression(node) {
|
|
55
57
|
output.replaceLeft(node.start, node.end, asReference(node));
|
|
58
|
+
},
|
|
59
|
+
ImportSpecifier(node) {
|
|
60
|
+
const inode = node;
|
|
61
|
+
const prefix = inode.view ? "viewof$" : inode.mutable ? "mutable$" : null;
|
|
62
|
+
if (prefix) {
|
|
63
|
+
const imported = asImportName(node.imported);
|
|
64
|
+
output.replaceLeft(node.start, node.imported.start, prefix);
|
|
65
|
+
if (node.imported === node.local) {
|
|
66
|
+
output.insertLeft(node.start, `${imported},`);
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
const local = asImportName(node.local);
|
|
70
|
+
output.insertLeft(node.start, `${imported} as ${local},`);
|
|
71
|
+
output.insertLeft(node.local.start, prefix);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
56
74
|
}
|
|
57
75
|
});
|
|
58
76
|
}
|
|
77
|
+
function asImportName(ref) {
|
|
78
|
+
return ref.type === "Identifier" ? ref.name : ref.raw;
|
|
79
|
+
}
|
|
59
80
|
function asReference(ref) {
|
|
60
81
|
return ref.type === "ViewExpression"
|
|
61
82
|
? `viewof$${ref.id.name}`
|
|
@@ -1,3 +1,6 @@
|
|
|
1
1
|
import type { TemplateLiteral } from "acorn";
|
|
2
|
+
import type { Cell } from "../lib/notebook.js";
|
|
2
3
|
export declare function parseTemplate(input: string): TemplateLiteral;
|
|
4
|
+
/** @deprecated */
|
|
3
5
|
export declare function transpileTemplate(input: string, tag?: string, raw?: boolean): string;
|
|
6
|
+
export declare function transpileTemplate(cell: Cell): string;
|
|
@@ -70,6 +70,13 @@ export function parseTemplate(input) {
|
|
|
70
70
|
return TemplateCellParser.parse(input, acornOptions);
|
|
71
71
|
}
|
|
72
72
|
export function transpileTemplate(input, tag = "", raw = false) {
|
|
73
|
+
let cell;
|
|
74
|
+
if (typeof input !== "string") {
|
|
75
|
+
cell = input;
|
|
76
|
+
input = cell.value;
|
|
77
|
+
tag = cell.mode === "tex" ? "tex.block" : cell.mode === "sql" ? getSqlTag(cell) : cell.mode;
|
|
78
|
+
raw = cell.mode !== "md";
|
|
79
|
+
}
|
|
73
80
|
if (!input)
|
|
74
81
|
return input;
|
|
75
82
|
const source = new Sourcemap(input);
|
|
@@ -78,7 +85,16 @@ export function transpileTemplate(input, tag = "", raw = false) {
|
|
|
78
85
|
source.insertLeft(node.start, "`");
|
|
79
86
|
source.insertRight(node.end, "`");
|
|
80
87
|
source.insertLeft(node.start, tag);
|
|
81
|
-
|
|
88
|
+
let output = String(source);
|
|
89
|
+
if (cell?.mode === "sql" && !cell.hidden)
|
|
90
|
+
output += ".then(Inputs.table)";
|
|
91
|
+
return output;
|
|
92
|
+
}
|
|
93
|
+
function getSqlTag(cell) {
|
|
94
|
+
const { id, database = "var:db", since } = cell;
|
|
95
|
+
return database.startsWith("var:")
|
|
96
|
+
? `${database.slice("var:".length)}.sql`
|
|
97
|
+
: `DatabaseClient(${JSON.stringify(database)}, {id: ${id}${since === undefined ? "" : `, since: ${JSON.stringify(since)}`}}).sql`;
|
|
82
98
|
}
|
|
83
99
|
function escapeTemplateElements(source, node) {
|
|
84
100
|
for (const quasi of node.quasis) {
|
|
@@ -10,9 +10,9 @@ export type TranspiledJavaScript = {
|
|
|
10
10
|
output?: string;
|
|
11
11
|
/** whether to implicitly display the body value (e.g., an expression) */
|
|
12
12
|
autodisplay?: boolean;
|
|
13
|
-
/** whether to implicitly derive a view;
|
|
13
|
+
/** whether to implicitly derive a view; requires viewof output */
|
|
14
14
|
autoview?: boolean;
|
|
15
|
-
/** whether to implicitly derive a mutable;
|
|
15
|
+
/** whether to implicitly derive a mutable; requires mutable output */
|
|
16
16
|
automutable?: boolean;
|
|
17
17
|
};
|
|
18
18
|
export type TranspileOptions = {
|
|
@@ -21,5 +21,7 @@ export type TranspileOptions = {
|
|
|
21
21
|
/** If true, resolve file using import.meta.url (so Vite treats it as an asset). */
|
|
22
22
|
resolveFiles?: boolean;
|
|
23
23
|
};
|
|
24
|
+
/** @deprecated */
|
|
24
25
|
export declare function transpile(input: string, mode: Cell["mode"], options?: TranspileOptions): TranspiledJavaScript;
|
|
26
|
+
export declare function transpile(input: Cell, options?: TranspileOptions): TranspiledJavaScript;
|
|
25
27
|
export declare function transpileJavaScript(input: string, options?: TranspileOptions): TranspiledJavaScript;
|
|
@@ -1,20 +1,39 @@
|
|
|
1
|
+
import { toCell } from "../lib/notebook.js";
|
|
1
2
|
import { rewriteFileExpressions } from "./files.js";
|
|
2
|
-
import { hasImportDeclaration
|
|
3
|
+
import { hasImportDeclaration } from "./imports.js";
|
|
4
|
+
import { rewriteImportDeclarations, rewriteImportExpressions } from "./imports.js";
|
|
3
5
|
import { transpileObservable } from "./observable.js";
|
|
4
6
|
import { parseJavaScript } from "./parse.js";
|
|
5
7
|
import { Sourcemap } from "./sourcemap.js";
|
|
6
8
|
import { transpileTemplate } from "./template.js";
|
|
7
9
|
export function transpile(input, mode, options) {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
10
|
+
let cell;
|
|
11
|
+
if (typeof input === "string") {
|
|
12
|
+
mode = mode;
|
|
13
|
+
cell = toCell({ id: -1, value: input, mode });
|
|
14
|
+
}
|
|
15
|
+
else {
|
|
16
|
+
options = mode;
|
|
17
|
+
mode = input.mode;
|
|
18
|
+
cell = input;
|
|
19
|
+
input = cell.value;
|
|
20
|
+
}
|
|
21
|
+
const transpiled = mode === "ojs"
|
|
22
|
+
? transpileObservable(input, options)
|
|
23
|
+
: mode !== "js"
|
|
24
|
+
? transpileJavaScript(transpileTemplate(cell), options)
|
|
25
|
+
: transpileJavaScript(input, options);
|
|
26
|
+
if (transpiled.output === undefined)
|
|
27
|
+
transpiled.output = cell.output;
|
|
28
|
+
if (cell.hidden)
|
|
29
|
+
transpiled.autodisplay = false;
|
|
30
|
+
else if (mode !== "js" && mode !== "ojs") {
|
|
31
|
+
transpiled.autodisplay = !!input;
|
|
32
|
+
transpiled.autoview = mode === "sql" && transpiled.autodisplay && !!transpiled.output;
|
|
33
|
+
if (transpiled.autoview)
|
|
34
|
+
transpiled.output = `viewof$${transpiled.output}`;
|
|
35
|
+
}
|
|
36
|
+
return transpiled;
|
|
18
37
|
}
|
|
19
38
|
export function transpileJavaScript(input, options) {
|
|
20
39
|
const cell = parseJavaScript(input);
|
|
@@ -6,6 +6,13 @@ it("transpiles JavaScript expressions", () => {
|
|
|
6
6
|
expect(transpile("await z", "js")).toMatchSnapshot();
|
|
7
7
|
expect(transpile("display(1), display(2)", "js")).toMatchSnapshot();
|
|
8
8
|
});
|
|
9
|
+
it("transpiles empty cells", () => {
|
|
10
|
+
expect(transpile("", "js")).toMatchSnapshot();
|
|
11
|
+
expect(transpile("", "md")).toMatchSnapshot();
|
|
12
|
+
expect(transpile("", "html")).toMatchSnapshot();
|
|
13
|
+
expect(transpile("", "tex")).toMatchSnapshot();
|
|
14
|
+
expect(transpile("", "sql")).toMatchSnapshot();
|
|
15
|
+
});
|
|
9
16
|
it("transpiles JavaScript programs", () => {
|
|
10
17
|
expect(transpile("const x = 1, y = 2;", "js")).toMatchSnapshot();
|
|
11
18
|
expect(transpile("x + y;", "js")).toMatchSnapshot();
|
|
@@ -21,6 +28,15 @@ it("transpiles dynamic npm: imports", () => {
|
|
|
21
28
|
});
|
|
22
29
|
it("transpiles static observable: imports", () => {
|
|
23
30
|
expect(transpile('import {Scrubber} from "observable:@mbostock/scrubber";', "js")).toMatchSnapshot();
|
|
31
|
+
expect(transpile('import {viewof$rotation} from "observable:@rreusser/drawing-3d-objects-with-svg";', "js")).toMatchSnapshot();
|
|
32
|
+
});
|
|
33
|
+
it("transpiles static imports with {type: 'observable'}", () => {
|
|
34
|
+
expect(transpile('import {Scrubber} from "https://api.observablehq.com/@mbostock/scrubber.js?v=4" with {type: "observable"};', "js")).toMatchSnapshot();
|
|
35
|
+
expect(transpile('import {viewof$rotation} from "https://api.observablehq.com/@rreusser/drawing-3d-objects-with-svg.js?v=4" with {type: "observable"};', "js")).toMatchSnapshot();
|
|
36
|
+
});
|
|
37
|
+
it("transpiles Observable JavaScript imports", () => {
|
|
38
|
+
expect(transpile('import {figure, viewof rotation} from "@rreusser/drawing-3d-objects-with-svg"', "ojs")).toMatchSnapshot();
|
|
39
|
+
expect(transpile('import {figure, viewof rotation as rot} from "@rreusser/drawing-3d-objects-with-svg"', "ojs")).toMatchSnapshot();
|
|
24
40
|
});
|
|
25
41
|
it("transpiles import.meta.resolve", () => {
|
|
26
42
|
expect(transpile('import.meta.resolve("npm:d3")', "js")).toMatchSnapshot();
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { sluggify } from "./sluggify.js";
|
|
2
|
+
async function sha256(input) {
|
|
3
|
+
const encoded = new TextEncoder().encode(input);
|
|
4
|
+
const buffer = await crypto.subtle.digest("SHA-256", encoded);
|
|
5
|
+
return new Uint8Array(buffer).reduce((i, byte) => (i << 8n) | BigInt(byte), 0n);
|
|
6
|
+
}
|
|
7
|
+
function base36(int, length) {
|
|
8
|
+
return int.toString(36).padStart(length, "0").slice(0, length);
|
|
9
|
+
}
|
|
10
|
+
export async function hash(strings, ...params) {
|
|
11
|
+
return base36(await sha256(JSON.stringify([strings, ...params])), 16);
|
|
12
|
+
}
|
|
13
|
+
export async function nameHash(name) {
|
|
14
|
+
return /^[\w-]+$/.test(name)
|
|
15
|
+
? name
|
|
16
|
+
: `${sluggify(basename(name))}.${base36(await sha256(name), 8)}`;
|
|
17
|
+
}
|
|
18
|
+
function basename(name) {
|
|
19
|
+
return name.replace(/^.*\//, "");
|
|
20
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { assert, describe, test } from "vitest";
|
|
2
|
+
import { hash, nameHash } from "./hash.js";
|
|
3
|
+
describe("nameHash", () => {
|
|
4
|
+
test("returns a simple name as-is", async () => {
|
|
5
|
+
assert.strictEqual(await nameHash("foo"), "foo");
|
|
6
|
+
assert.strictEqual(await nameHash("foo-bar"), "foo-bar");
|
|
7
|
+
assert.strictEqual(await nameHash("foo-"), "foo-");
|
|
8
|
+
assert.strictEqual(await nameHash("-foo"), "-foo");
|
|
9
|
+
});
|
|
10
|
+
test("sluggifies and hashes names with special characters", async () => {
|
|
11
|
+
assert.strictEqual(await nameHash("foo.db"), "foo-db.2s9flvsm");
|
|
12
|
+
assert.strictEqual(await nameHash("./foo.db"), "foo-db.3ee6cmxd");
|
|
13
|
+
assert.strictEqual(await nameHash("data/foo.db"), "foo-db.61nrbwb0");
|
|
14
|
+
assert.strictEqual(await nameHash("bar/foo.db"), "foo-db.1jlqjad7");
|
|
15
|
+
assert.strictEqual(await nameHash("foo bar"), "foo-bar.69w36b7f");
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
describe("hash", () => {
|
|
19
|
+
test("returns the expected static hash", async () => {
|
|
20
|
+
assert.strictEqual(await hash `foo`, "1n7k0l3ilxvth9al");
|
|
21
|
+
assert.strictEqual(await hash `bar`, "39v7mmkf7sfehxh1");
|
|
22
|
+
assert.strictEqual(await hash ``, "gepym5nmvuej8503");
|
|
23
|
+
});
|
|
24
|
+
test("returns the expected dynamic hash", async () => {
|
|
25
|
+
assert.strictEqual(await hash `SELECT 1 + ${2}`, "64iqby4orqj5tgek");
|
|
26
|
+
assert.strictEqual(await hash `SELECT 1 + ${3}`, "1azi8mazfb39kln7");
|
|
27
|
+
});
|
|
28
|
+
});
|
|
@@ -24,12 +24,22 @@ export interface CellSpec {
|
|
|
24
24
|
mode?: "js" | "ojs" | "md" | "html" | "tex" | "dot" | "sql";
|
|
25
25
|
/** if true, the editor will stay open when not focused; defaults to false */
|
|
26
26
|
pinned?: boolean;
|
|
27
|
+
/** if true, implicit display will be suppressed; defaults to false */
|
|
28
|
+
hidden?: boolean;
|
|
29
|
+
/** if present, exposes the cell’s value to the rest of the notebook */
|
|
30
|
+
output?: string;
|
|
31
|
+
/** for SQL cells, the database to query; use var:<name> to refer to a variable */
|
|
32
|
+
database?: string;
|
|
33
|
+
/** for SQL cells, the oldest allowable age of the cached query result */
|
|
34
|
+
since?: Date | string | number;
|
|
27
35
|
}
|
|
28
36
|
export interface Cell extends CellSpec {
|
|
29
37
|
value: NonNullable<CellSpec["value"]>;
|
|
30
38
|
mode: NonNullable<CellSpec["mode"]>;
|
|
31
39
|
pinned: NonNullable<CellSpec["pinned"]>;
|
|
40
|
+
hidden: NonNullable<CellSpec["hidden"]>;
|
|
41
|
+
since?: Date;
|
|
32
42
|
}
|
|
33
43
|
export declare function toNotebook({ cells, title, theme, readOnly }: NotebookSpec): Notebook;
|
|
34
|
-
export declare function toCell({ id, value, mode, pinned }: CellSpec): Cell;
|
|
44
|
+
export declare function toCell({ id, value, mode, pinned, hidden, output, database, since }: CellSpec): Cell;
|
|
35
45
|
export declare function defaultPinned(mode: Cell["mode"]): boolean;
|
package/dist/src/lib/notebook.js
CHANGED
|
@@ -6,14 +6,21 @@ export function toNotebook({ cells = [], title = "Untitled", theme = "air", read
|
|
|
6
6
|
readOnly
|
|
7
7
|
};
|
|
8
8
|
}
|
|
9
|
-
export function toCell({ id, value = "", mode = "js", pinned = defaultPinned(mode) }) {
|
|
9
|
+
export function toCell({ id, value = "", mode = "js", pinned = defaultPinned(mode), hidden = false, output, database = mode === "sql" ? "var:db" : undefined, since }) {
|
|
10
10
|
return {
|
|
11
11
|
id,
|
|
12
12
|
value,
|
|
13
13
|
mode,
|
|
14
|
-
pinned
|
|
14
|
+
pinned,
|
|
15
|
+
hidden,
|
|
16
|
+
output,
|
|
17
|
+
database: mode === "sql" ? database : undefined,
|
|
18
|
+
since: since !== undefined ? asDate(since) : undefined
|
|
15
19
|
};
|
|
16
20
|
}
|
|
21
|
+
function asDate(date) {
|
|
22
|
+
return date instanceof Date ? date : new Date(date);
|
|
23
|
+
}
|
|
17
24
|
export function defaultPinned(mode) {
|
|
18
|
-
return mode === "js" || mode === "ojs";
|
|
25
|
+
return mode === "js" || mode === "sql" || mode === "ojs";
|
|
19
26
|
}
|
|
@@ -13,7 +13,11 @@ test("converts a cell spec to a cell", () => {
|
|
|
13
13
|
id: 1,
|
|
14
14
|
value: "",
|
|
15
15
|
mode: "js",
|
|
16
|
-
pinned: true
|
|
16
|
+
pinned: true,
|
|
17
|
+
hidden: false,
|
|
18
|
+
output: undefined,
|
|
19
|
+
database: undefined,
|
|
20
|
+
since: undefined
|
|
17
21
|
});
|
|
18
22
|
});
|
|
19
23
|
test("computes an appropriate default pinned based on the cell mode", () => {
|
|
@@ -21,6 +25,10 @@ test("computes an appropriate default pinned based on the cell mode", () => {
|
|
|
21
25
|
id: 1,
|
|
22
26
|
value: "",
|
|
23
27
|
mode: "md",
|
|
24
|
-
pinned: false
|
|
28
|
+
pinned: false,
|
|
29
|
+
hidden: false,
|
|
30
|
+
output: undefined,
|
|
31
|
+
database: undefined,
|
|
32
|
+
since: undefined
|
|
25
33
|
});
|
|
26
34
|
});
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { Notebook } from "./notebook.js";
|
|
2
|
-
export declare function serialize(notebook: Notebook
|
|
2
|
+
export declare function serialize(notebook: Notebook, { document }?: {
|
|
3
|
+
document?: Document | undefined;
|
|
4
|
+
}): string;
|
|
3
5
|
export declare function deserialize(data: string, { parser }?: {
|
|
4
6
|
parser?: DOMParser | undefined;
|
|
5
7
|
}): Notebook;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { toNotebook } from "./notebook.js";
|
|
2
2
|
import { isEmpty } from "./text.js";
|
|
3
|
-
export function serialize(notebook) {
|
|
3
|
+
export function serialize(notebook, { document = globalThis.document } = {}) {
|
|
4
4
|
const _notebook = document.createElement("notebook");
|
|
5
5
|
_notebook.setAttribute("theme", notebook.theme);
|
|
6
6
|
if (notebook.readOnly)
|
|
@@ -17,6 +17,12 @@ export function serialize(notebook) {
|
|
|
17
17
|
_cell.textContent = indent(cell.value.replace(/<(?=\\*\/script(\s|>))/gi, "<\\"));
|
|
18
18
|
if (cell.pinned)
|
|
19
19
|
_cell.setAttribute("pinned", "");
|
|
20
|
+
if (cell.hidden)
|
|
21
|
+
_cell.setAttribute("hidden", "");
|
|
22
|
+
if (cell.database)
|
|
23
|
+
_cell.setAttribute("database", cell.database);
|
|
24
|
+
if (cell.output)
|
|
25
|
+
_cell.setAttribute("output", cell.output);
|
|
20
26
|
_notebook.appendChild(_cell);
|
|
21
27
|
}
|
|
22
28
|
_notebook.appendChild(document.createTextNode("\n"));
|
|
@@ -37,10 +43,13 @@ export function deserialize(data, { parser = new DOMParser() } = {}) {
|
|
|
37
43
|
else if (id > maxCellId)
|
|
38
44
|
maxCellId = id;
|
|
39
45
|
cellIds.add(id);
|
|
40
|
-
const pinned = cell.hasAttribute("pinned");
|
|
41
|
-
const value = dedent(cell.textContent?.replace(/<\\(?=\\*\/script(\s|>))/gi, "<") ?? "");
|
|
42
46
|
const mode = deserializeMode(cell.getAttribute("type"));
|
|
43
|
-
|
|
47
|
+
const value = dedent(cell.textContent?.replace(/<\\(?=\\*\/script(\s|>))/gi, "<") ?? "");
|
|
48
|
+
const pinned = cell.hasAttribute("pinned");
|
|
49
|
+
const hidden = cell.hasAttribute("hidden");
|
|
50
|
+
const output = cell.getAttribute("output") ?? undefined;
|
|
51
|
+
const database = cell.getAttribute("database") ?? undefined;
|
|
52
|
+
return { id, mode, value, pinned, hidden, output, database };
|
|
44
53
|
});
|
|
45
54
|
return toNotebook({ title, theme, readOnly, cells });
|
|
46
55
|
}
|
|
@@ -1,7 +1,14 @@
|
|
|
1
|
-
|
|
1
|
+
import { JSDOM } from "jsdom";
|
|
2
2
|
import { assert, test } from "vitest";
|
|
3
3
|
import { toNotebook } from "./notebook.js";
|
|
4
|
-
import { deserialize, serialize } from "./serialize.js";
|
|
4
|
+
import { deserialize as _deserialize, serialize as _serialize } from "./serialize.js";
|
|
5
|
+
const { window } = new JSDOM();
|
|
6
|
+
function serialize(notebook) {
|
|
7
|
+
return _serialize(notebook, { document: window.document });
|
|
8
|
+
}
|
|
9
|
+
function deserialize(data) {
|
|
10
|
+
return _deserialize(data, { parser: new window.DOMParser() });
|
|
11
|
+
}
|
|
5
12
|
test("serializes unpinned cells", () => {
|
|
6
13
|
const notebook1 = toNotebook({
|
|
7
14
|
cells: [
|
|
@@ -84,7 +91,7 @@ test("serialization escapes </script>, in various forms", () => {
|
|
|
84
91
|
{ id: 6, mode: "js", pinned: true, value: `'<\\/script>'` },
|
|
85
92
|
{ id: 7, mode: "js", pinned: true, value: `'<\\/script '` },
|
|
86
93
|
{ id: 8, mode: "js", pinned: true, value: `'<\\\\/SCRIPT '` },
|
|
87
|
-
{ id: 9, mode: "js", pinned: true, value: `'<\\\\/sCrIpT '` }
|
|
94
|
+
{ id: 9, mode: "js", pinned: true, value: `'<\\\\/sCrIpT '` }
|
|
88
95
|
]
|
|
89
96
|
});
|
|
90
97
|
const html = serialize(notebook1);
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export function sluggify(string, { length = 50, fallback = "untitled", separator = "-" } = {}) {
|
|
2
|
+
const parts = string
|
|
3
|
+
.normalize("NFD")
|
|
4
|
+
.replace(/[\u0300-\u036f'‘’]/g, "")
|
|
5
|
+
.toLowerCase()
|
|
6
|
+
.split(/\W+/g)
|
|
7
|
+
.filter(nonempty);
|
|
8
|
+
let i = -1;
|
|
9
|
+
for (let l = 0, n = parts.length; ++i < n;) {
|
|
10
|
+
if ((l += parts[i].length) + i > length) {
|
|
11
|
+
parts[i] = parts[i].substring(0, length - l + parts[i].length - i);
|
|
12
|
+
break;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
return (parts
|
|
16
|
+
.slice(0, i + 1)
|
|
17
|
+
.filter(Boolean)
|
|
18
|
+
.join(separator) || fallback.slice(0, length));
|
|
19
|
+
}
|
|
20
|
+
function nonempty(string) {
|
|
21
|
+
return string.length > 0;
|
|
22
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { assert, test } from "vitest";
|
|
2
|
+
import { sluggify } from "./sluggify.js";
|
|
3
|
+
test("returns the default fallback for empty slugs", () => {
|
|
4
|
+
assert.strictEqual(sluggify(""), "untitled");
|
|
5
|
+
assert.strictEqual(sluggify(" "), "untitled");
|
|
6
|
+
assert.strictEqual(sluggify("---"), "untitled");
|
|
7
|
+
assert.strictEqual(sluggify("##)!@#(*"), "untitled");
|
|
8
|
+
});
|
|
9
|
+
test("returns the given fallback for empty slugs", () => {
|
|
10
|
+
assert.strictEqual(sluggify("", { fallback: "foo" }), "foo");
|
|
11
|
+
assert.strictEqual(sluggify(" ", { fallback: "foo" }), "foo");
|
|
12
|
+
assert.strictEqual(sluggify("---", { fallback: "foo" }), "foo");
|
|
13
|
+
assert.strictEqual(sluggify("##)!@#(*", { fallback: "foo" }), "foo");
|
|
14
|
+
});
|
|
15
|
+
test("lowercases", () => {
|
|
16
|
+
assert.strictEqual(sluggify("HELLO WORLD"), "hello-world");
|
|
17
|
+
assert.strictEqual(sluggify("HelLo WorlD"), "hello-world");
|
|
18
|
+
});
|
|
19
|
+
test("removes emoji", () => {
|
|
20
|
+
assert.strictEqual(sluggify("HELLO 😎"), "hello");
|
|
21
|
+
assert.strictEqual(sluggify("HELLO 😎 world"), "hello-world");
|
|
22
|
+
assert.strictEqual(sluggify("HELLO 💩 world"), "hello-world");
|
|
23
|
+
});
|
|
24
|
+
test("trims leading and trailing spaces", () => {
|
|
25
|
+
assert.strictEqual(sluggify(" hello world "), "hello-world");
|
|
26
|
+
});
|
|
27
|
+
test("collapses contiguous spaces", () => {
|
|
28
|
+
assert.strictEqual(sluggify(" hello world "), "hello-world");
|
|
29
|
+
});
|
|
30
|
+
test("removes punctuation", () => {
|
|
31
|
+
assert.strictEqual(sluggify("Hello, world!"), "hello-world");
|
|
32
|
+
assert.strictEqual(sluggify("Hello, 'world'!"), "hello-world");
|
|
33
|
+
assert.strictEqual(sluggify('Hello, "world"!'), "hello-world");
|
|
34
|
+
assert.strictEqual(sluggify("Hello, “world”!"), "hello-world");
|
|
35
|
+
assert.strictEqual(sluggify("Hello, ‘world’!"), "hello-world");
|
|
36
|
+
assert.strictEqual(sluggify("Hello, fo'c's'le!"), "hello-focsle");
|
|
37
|
+
assert.strictEqual(sluggify("Hello, fo’c’s’le!"), "hello-focsle");
|
|
38
|
+
});
|
|
39
|
+
test("removes diacritics and combiners", () => {
|
|
40
|
+
assert.strictEqual(sluggify("Héllö, wørld!"), "hello-w-rld");
|
|
41
|
+
assert.strictEqual(sluggify("z̷̢̡̟͍̺͛͆͐̀ą̸̻̰̪͈͒͝ͅl̸͇̘̓g̶̡͈͒̾̉̽̑̅ö̸̧̟́͆"), "zalgo");
|
|
42
|
+
});
|
|
43
|
+
test("allows up to 50 characters after stripping", () => {
|
|
44
|
+
assert.strictEqual(sluggify("‘A‘ohe pu‘u ki‘eki‘e ke ho ‘ā‘o ‘ia e pi‘i"), "aohe-puu-kiekie-ke-ho-ao-ia-e-pii");
|
|
45
|
+
assert.strictEqual(sluggify("0123456789012345678901234567890123456789012345678"), "0123456789012345678901234567890123456789012345678");
|
|
46
|
+
assert.strictEqual(sluggify("01234567890123456789012345678901234567890123456789"), "01234567890123456789012345678901234567890123456789");
|
|
47
|
+
assert.strictEqual(sluggify("012345678901234567890123456789012345678901234567890"), "01234567890123456789012345678901234567890123456789");
|
|
48
|
+
assert.strictEqual(sluggify("01234567890 1234567890 1234567890 1234567890 12345678"), "01234567890-1234567890-1234567890-1234567890-12345");
|
|
49
|
+
assert.strictEqual(sluggify("01234567890 1234567890 1234567890 1234567890 123456789"), "01234567890-1234567890-1234567890-1234567890-12345");
|
|
50
|
+
assert.strictEqual(sluggify("01234567890 1234567890 1234567890 1234567890 1234567890"), "01234567890-1234567890-1234567890-1234567890-12345");
|
|
51
|
+
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Variable, VariableDefinition } from "@observablehq/runtime";
|
|
1
|
+
import type { Module, Variable, VariableDefinition } from "@observablehq/runtime";
|
|
2
2
|
import type { DisplayState } from "./display.js";
|
|
3
3
|
import { observe } from "./display.js";
|
|
4
4
|
export type DefineState = DisplayState & {
|
|
@@ -25,4 +25,4 @@ export type Definition = {
|
|
|
25
25
|
/** an asset mapping to apply to any autodisplayed assets (e.g., images and videos) */
|
|
26
26
|
assets?: Map<string, string>;
|
|
27
27
|
};
|
|
28
|
-
export declare function define(state: DefineState, definition: Definition, observer?: typeof observe): void;
|
|
28
|
+
export declare function define(main: Module, state: DefineState, definition: Definition, observer?: typeof observe): void;
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { clear, display, observe } from "./display.js";
|
|
2
|
-
import { main } from "./index.js";
|
|
3
2
|
import { input } from "./stdlib/generators/index.js";
|
|
4
3
|
import { Mutator } from "./stdlib/mutable.js";
|
|
5
|
-
export function define(state, definition, observer = observe) {
|
|
4
|
+
export function define(main, state, definition, observer = observe) {
|
|
6
5
|
const { id, body, inputs = [], outputs = [], output, autodisplay, autoview, automutable } = definition;
|
|
7
6
|
const variables = state.variables;
|
|
8
7
|
const v = main.variable(observer(state, definition), { shadow: {} });
|
|
9
8
|
const vid = output ?? (outputs.length ? `cell ${id}` : null);
|
|
9
|
+
state.autoclear = true;
|
|
10
10
|
if (inputs.includes("display") || inputs.includes("view")) {
|
|
11
11
|
let displayVersion = -1; // the variable._version of currently-displayed values
|
|
12
12
|
const vd = new v.constructor(2, v._module);
|
|
@@ -2,6 +2,8 @@ import type { Definition } from "./define.js";
|
|
|
2
2
|
export type DisplayState = {
|
|
3
3
|
/** the HTML element in which to render this cell’s display */
|
|
4
4
|
root: HTMLDivElement;
|
|
5
|
+
/** whether to clear on fulfilled */
|
|
6
|
+
autoclear?: boolean;
|
|
5
7
|
/** for inspected values, any expanded paths; see getExpanded */
|
|
6
8
|
expanded: (number[][] | undefined)[];
|
|
7
9
|
};
|
|
@@ -28,6 +28,7 @@ function isDisplayable(value, root) {
|
|
|
28
28
|
(!value.parentNode || root.contains(value)));
|
|
29
29
|
}
|
|
30
30
|
export function clear(state) {
|
|
31
|
+
state.autoclear = false;
|
|
31
32
|
state.expanded = Array.from(state.root.childNodes, getExpanded);
|
|
32
33
|
while (state.root.lastChild)
|
|
33
34
|
state.root.lastChild.remove();
|
|
@@ -44,11 +45,14 @@ export function observe(state, { autodisplay, assets }) {
|
|
|
44
45
|
},
|
|
45
46
|
fulfilled(value) {
|
|
46
47
|
if (autodisplay) {
|
|
47
|
-
clear(state);
|
|
48
48
|
if (assets && value instanceof Element)
|
|
49
49
|
mapAssets(value, assets);
|
|
50
|
+
clear(state);
|
|
50
51
|
display(state, value);
|
|
51
52
|
}
|
|
53
|
+
else if (state.autoclear) {
|
|
54
|
+
clear(state);
|
|
55
|
+
}
|
|
52
56
|
},
|
|
53
57
|
rejected(error) {
|
|
54
58
|
console.error(error);
|