@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.
Files changed (60) hide show
  1. package/dist/bin/build.js +7 -0
  2. package/dist/bin/download.js +15 -19
  3. package/dist/package.json +31 -2
  4. package/dist/src/databases/duckdb.d.ts +2 -0
  5. package/dist/src/databases/duckdb.js +72 -0
  6. package/dist/src/databases/index.d.ts +46 -0
  7. package/dist/src/databases/index.js +54 -0
  8. package/dist/src/databases/options.d.ts +3 -0
  9. package/dist/src/databases/options.js +6 -0
  10. package/dist/src/databases/postgres.d.ts +2 -0
  11. package/dist/src/databases/postgres.js +95 -0
  12. package/dist/src/databases/snowflake.d.ts +2 -0
  13. package/dist/src/databases/snowflake.js +102 -0
  14. package/dist/src/databases/sqlite.d.ts +2 -0
  15. package/dist/src/databases/sqlite.js +64 -0
  16. package/dist/src/javascript/imports/npm.js +2 -0
  17. package/dist/src/javascript/observable.js +24 -3
  18. package/dist/src/javascript/template.d.ts +3 -0
  19. package/dist/src/javascript/template.js +17 -1
  20. package/dist/src/javascript/transpile.d.ts +4 -2
  21. package/dist/src/javascript/transpile.js +30 -11
  22. package/dist/src/javascript/transpile.test.js +16 -0
  23. package/dist/src/lib/error.d.ts +6 -0
  24. package/dist/src/lib/error.js +6 -0
  25. package/dist/src/lib/hash.d.ts +2 -0
  26. package/dist/src/lib/hash.js +20 -0
  27. package/dist/src/lib/hash.test.d.ts +1 -0
  28. package/dist/src/lib/hash.test.js +28 -0
  29. package/dist/src/lib/notebook.d.ts +11 -1
  30. package/dist/src/lib/notebook.js +10 -3
  31. package/dist/src/lib/notebook.test.js +10 -2
  32. package/dist/src/lib/serialize.d.ts +3 -1
  33. package/dist/src/lib/serialize.js +13 -4
  34. package/dist/src/lib/serialize.test.js +10 -3
  35. package/dist/src/lib/sluggify.d.ts +6 -0
  36. package/dist/src/lib/sluggify.js +22 -0
  37. package/dist/src/lib/sluggify.test.d.ts +1 -0
  38. package/dist/src/lib/sluggify.test.js +51 -0
  39. package/dist/src/runtime/define.d.ts +2 -2
  40. package/dist/src/runtime/define.js +2 -2
  41. package/dist/src/runtime/display.d.ts +2 -0
  42. package/dist/src/runtime/display.js +5 -1
  43. package/dist/src/runtime/index.d.ts +100 -1
  44. package/dist/src/runtime/index.js +29 -3
  45. package/dist/src/runtime/stdlib/databaseClient.d.ts +45 -0
  46. package/dist/src/runtime/stdlib/databaseClient.js +77 -0
  47. package/dist/src/runtime/stdlib/duckdb.js +7 -4
  48. package/dist/src/runtime/stdlib/fileAttachment.d.ts +38 -11
  49. package/dist/src/runtime/stdlib/fileAttachment.js +21 -10
  50. package/dist/src/runtime/stdlib/generators/input.d.ts +1 -1
  51. package/dist/src/runtime/stdlib/index.d.ts +43 -6
  52. package/dist/src/runtime/stdlib/index.js +5 -5
  53. package/dist/src/styles/global.css +1 -1
  54. package/dist/src/templates/default.html +18 -18
  55. package/dist/src/vite/config.js +5 -2
  56. package/dist/src/vite/observable.d.ts +25 -6
  57. package/dist/src/vite/observable.js +42 -15
  58. package/package.json +31 -2
  59. package/dist/src/runtime/stdlib/sql.d.ts +0 -5
  60. 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
- // Rewrite viewof x ↦ viewof$x, and mutable x ↦ mutable$x.value.
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
- return String(source);
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; for ojs compatibility; requires viewof output */
13
+ /** whether to implicitly derive a view; requires viewof output */
14
14
  autoview?: boolean;
15
- /** whether to implicitly derive a mutable; for ojs compatibility; requires mutable output */
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, rewriteImportDeclarations, rewriteImportExpressions } from "./imports.js";
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
- return mode === "ojs"
9
- ? transpileObservable(input, options) // TODO ojs+md etc.
10
- : transpileJavaScript(transpileMode(input, mode), options);
11
- }
12
- function transpileMode(input, mode) {
13
- if (mode === "js")
14
- return input;
15
- const tag = mode === "tex" ? "tex.block" : mode === "sql" ? "__sql(db, Inputs.table)" : mode; // for now
16
- const raw = mode === "html" || mode === "tex" || mode === "dot";
17
- return transpileTemplate(input, tag, raw);
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,6 @@
1
+ export declare function isEnoent(error: unknown): error is Error & {
2
+ code: "ENOENT";
3
+ };
4
+ export declare function isSystemError(error: unknown): error is Error & {
5
+ code: string;
6
+ };
@@ -0,0 +1,6 @@
1
+ export function isEnoent(error) {
2
+ return isSystemError(error) && error.code === "ENOENT";
3
+ }
4
+ export function isSystemError(error) {
5
+ return error instanceof Error && "code" in error;
6
+ }
@@ -0,0 +1,2 @@
1
+ export declare function hash(strings: readonly string[], ...params: unknown[]): Promise<string>;
2
+ export declare function nameHash(name: string): Promise<string>;
@@ -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;
@@ -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): string;
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
- return { id, pinned, mode, value };
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
- // @vitest-environment jsdom
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,6 @@
1
+ export interface SluggifyOptions {
2
+ length?: number;
3
+ fallback?: string;
4
+ separator?: string;
5
+ }
6
+ export declare function sluggify(string: string, { length, fallback, separator }?: SluggifyOptions): string;
@@ -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);