diff --git a/src/fileAttachment.js b/src/fileAttachment.js index 5cbb2b59..a1da5bb9 100644 --- a/src/fileAttachment.js +++ b/src/fileAttachment.js @@ -1,4 +1,5 @@ import {require as requireDefault} from "d3-require"; +import sqlite, {SQLiteDatabaseClient} from "./sqlite.js"; async function remote_fetch(file) { const response = await fetch(await file.url()); @@ -56,6 +57,11 @@ class FileAttachment { i.src = url; }); } + async sqlite() { + const [SQL, buffer] = await Promise.all([sqlite(requireDefault), this.arrayBuffer()]); + const db = new SQL.Database(new Uint8Array(buffer)); + return new SQLiteDatabaseClient(db); + } } export function NoFileAttachments(name) { diff --git a/src/library.js b/src/library.js index 70d1e97a..6abe513c 100644 --- a/src/library.js +++ b/src/library.js @@ -10,6 +10,7 @@ import now from "./now.js"; import Promises from "./promises/index.js"; import resolve from "./resolve.js"; import requirer from "./require.js"; +import SQLite from "./sqlite.js"; import svg from "./svg.js"; import tex from "./tex.js"; import vegalite from "./vegalite.js"; @@ -26,18 +27,19 @@ export default Object.assign(function Library(resolver) { Mutable: () => Mutable, Plot: () => require("@observablehq/plot@0.1.0/dist/plot.umd.min.js"), Promises: () => Promises, + SQLite: () => SQLite(require), _: () => require("lodash@4.17.21/lodash.min.js"), d3: () => require("d3@6.7.0/dist/d3.min.js"), dot: () => require("@observablehq/graphviz@0.2.1/dist/graphviz.min.js"), htl: () => require("htl@0.2.5/dist/htl.min.js"), html: () => html, - md: md(require), + md: () => md(require), now: now, require: () => require, resolve: () => resolve, svg: () => svg, - tex: tex(require), - vl: vegalite(require), + tex: () => tex(require), + vl: () => vegalite(require), width: width })); }, {resolve: requireDefault.resolve}); diff --git a/src/md.js b/src/md.js index 027b7087..a09c0f99 100644 --- a/src/md.js +++ b/src/md.js @@ -1,47 +1,44 @@ import template from "./template.js"; -const HL_ROOT = - "https://cdn.jsdelivr.net/npm/@observablehq/highlight.js@2.0.0/"; +const HL_ROOT = "https://cdn.jsdelivr.net/npm/@observablehq/highlight.js@2.0.0/"; export default function(require) { - return function() { - return require("marked@0.3.12/marked.min.js").then(function(marked) { - return template( - function(string) { - var root = document.createElement("div"); - root.innerHTML = marked(string, {langPrefix: ""}).trim(); - var code = root.querySelectorAll("pre code[class]"); - if (code.length > 0) { - require(HL_ROOT + "highlight.min.js").then(function(hl) { - code.forEach(function(block) { - function done() { - hl.highlightBlock(block); - block.parentNode.classList.add("observablehq--md-pre"); - } - if (hl.getLanguage(block.className)) { - done(); - } else { - require(HL_ROOT + "async-languages/index.js") - .then(index => { - if (index.has(block.className)) { - return require(HL_ROOT + - "async-languages/" + - index.get(block.className)).then(language => { - hl.registerLanguage(block.className, language); - }); - } - }) - .then(done, done); - } - }); + return require("marked@0.3.12/marked.min.js").then(function(marked) { + return template( + function(string) { + var root = document.createElement("div"); + root.innerHTML = marked(string, {langPrefix: ""}).trim(); + var code = root.querySelectorAll("pre code[class]"); + if (code.length > 0) { + require(HL_ROOT + "highlight.min.js").then(function(hl) { + code.forEach(function(block) { + function done() { + hl.highlightBlock(block); + block.parentNode.classList.add("observablehq--md-pre"); + } + if (hl.getLanguage(block.className)) { + done(); + } else { + require(HL_ROOT + "async-languages/index.js") + .then(index => { + if (index.has(block.className)) { + return require(HL_ROOT + + "async-languages/" + + index.get(block.className)).then(language => { + hl.registerLanguage(block.className, language); + }); + } + }) + .then(done, done); + } }); - } - return root; - }, - function() { - return document.createElement("div"); + }); } - ); - }); - }; + return root; + }, + function() { + return document.createElement("div"); + } + ); + }); } diff --git a/src/sqlite.js b/src/sqlite.js new file mode 100644 index 00000000..af36a68b --- /dev/null +++ b/src/sqlite.js @@ -0,0 +1,56 @@ +export default async function sqlite(require) { + const sql = await require("sql.js@1.5.0/dist/sql-wasm.js"); + return sql({locateFile: file => `https://cdn.jsdelivr.net/npm/sql.js@1.5.0/dist/${file}`}); +} + +export class SQLiteDatabaseClient { + constructor(db) { + Object.defineProperties(this, { + _db: {value: db} + }); + } + async query(query, params) { + return await exec(this._db, query, params); + } + async queryRow(query, params) { + return (await this.query(query, params))[0] || null; + } + async explain(query, params) { + const rows = await this.query(`EXPLAIN QUERY PLAN ${query}`, params); + return element("pre", {className: "observablehq--inspect"}, [ + text(rows.map(row => row.detail).join("\n")) + ]); + } + async describe(object) { + const rows = await (object === undefined + ? this.query(`SELECT name FROM sqlite_master WHERE type = 'table'`) + : this.query(`SELECT * FROM pragma_table_info(?)`, [object])); + if (!rows.length) throw new Error("Not found"); + const {columns} = rows; + return element("table", {value: rows}, [ + element("thead", [element("tr", columns.map(c => element("th", [text(c)])))]), + element("tbody", rows.map(r => element("tr", columns.map(c => element("td", [text(r[c])]))))) + ]); + } +} + +async function exec(db, query, params) { + const [result] = await db.exec(query, params); + if (!result) return []; + const {columns, values} = result; + const rows = values.map(row => Object.fromEntries(row.map((value, i) => [columns[i], value]))); + rows.columns = columns; + return rows; +} + +function element(name, props, children) { + if (arguments.length === 2) children = props, props = undefined; + const element = document.createElement(name); + if (props !== undefined) for (const p in props) element[p] = props[p]; + if (children !== undefined) for (const c of children) element.appendChild(c); + return element; +} + +function text(value) { + return document.createTextNode(value); +} diff --git a/src/tex.js b/src/tex.js index cb5bc007..c07cea43 100644 --- a/src/tex.js +++ b/src/tex.js @@ -11,25 +11,23 @@ function style(href) { }); } -export default function(require) { - return function() { - return Promise.all([ - require("@observablehq/katex@0.11.1/dist/katex.min.js"), - require.resolve("@observablehq/katex@0.11.1/dist/katex.min.css").then(style) - ]).then(function(values) { - var katex = values[0], tex = renderer(); +export default function tex(require) { + return Promise.all([ + require("@observablehq/katex@0.11.1/dist/katex.min.js"), + require.resolve("@observablehq/katex@0.11.1/dist/katex.min.css").then(style) + ]).then(function(values) { + var katex = values[0], tex = renderer(); - function renderer(options) { - return function() { - var root = document.createElement("div"); - katex.render(raw.apply(String, arguments), root, options); - return root.removeChild(root.firstChild); - }; - } + function renderer(options) { + return function() { + var root = document.createElement("div"); + katex.render(raw.apply(String, arguments), root, options); + return root.removeChild(root.firstChild); + }; + } - tex.options = renderer; - tex.block = renderer({displayMode: true}); - return tex; - }); - }; + tex.options = renderer; + tex.block = renderer({displayMode: true}); + return tex; + }); } diff --git a/src/vegalite.js b/src/vegalite.js index 96ddc914..652a77f1 100644 --- a/src/vegalite.js +++ b/src/vegalite.js @@ -1,10 +1,8 @@ -export default function vl(require) { - return async () => { - const [vega, vegalite, api] = await Promise.all([ - "vega@5.20.2/build/vega.min.js", - "vega-lite@5.1.0/build/vega-lite.min.js", - "vega-lite-api@5.0.0/build/vega-lite-api.min.js" - ].map(module => require(module))); - return api.register(vega, vegalite); - }; +export default async function vl(require) { + const [vega, vegalite, api] = await Promise.all([ + "vega@5.20.2/build/vega.min.js", + "vega-lite@5.1.0/build/vega-lite.min.js", + "vega-lite-api@5.0.0/build/vega-lite-api.min.js" + ].map(module => require(module))); + return api.register(vega, vegalite); } diff --git a/test/index-test.js b/test/index-test.js index 8ff32a60..006bd53e 100644 --- a/test/index-test.js +++ b/test/index-test.js @@ -11,6 +11,7 @@ test("new Library returns a library with the expected keys", async t => { "Mutable", "Plot", "Promises", + "SQLite", "_", "d3", "dot",