diff --git a/README.md b/README.md index cb6715f1..3617db0b 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ For examples, see https://observablehq.com/@observablehq/standard-library. * [DOM](#dom) - create HTML and SVG elements. * [Files](#files) - read local files into memory. +* [FileAttachments](#file_attachments) - read remote files. * [Generators](#generators) - utilities for generators and iterators. * [Promises](#promises) - utilities for promises. * [require](#require) - load third-party libraries. @@ -273,6 +274,100 @@ A data URL may be significantly less efficient than [URL.createObjectURL](https: } ``` +### File Attachments + +See [File Attachments](https://observablehq.com/@observablehq/file-attachments) on Observable for examples. + +#
FileAttachments(resolve) [<>](https://github.com/observablehq/stdlib/blob/master/src/fileAttachment.js "Source") + +The **FileAttachments** function exported by the standard library is an abstract class that can be used to fetch files by name from remote URLs. To make it concrete, you call it with a *resolve* function, which is an async function that takes a *name* and returns a URL at which the file of that name may be loaded. For example: + +```js +const FileAttachment = FileAttachments((name) => + `https://my.server/notebooks/demo/${name}` +); +``` + +Or, with a more complex example, calling an API to produce temporary URLs: + +```js +const FileAttachment = FileAttachments(async (name) => + if (cachedUrls.has(name)) return cachedUrls.get(name); + const url = await fetchSignedFileUrl(notebookId, name); + cachedUrls.set(name, url); + return url; +); +``` + +Once you have your **FileAttachment** function defined, you can call it from notebook code: + +```js +photo = FileAttachment("sunset.jpg") +``` + +FileAttachments work similarly to the [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch), providing methods that return promises to the file’s contents in a handful of convenient forms. + +# FileAttachment(name).url() [<>](https://github.com/observablehq/stdlib/blob/master/src/fileAttachment.js "Source") + +Returns a promise to the URL at which the file may be retrieved. + +```js +const url = await FileAttachment("file.txt").url(); +``` + +# FileAttachment(name).text() [<>](https://github.com/observablehq/stdlib/blob/master/src/fileAttachment.js "Source") + +Returns a promise to the file’s contents as a JavaScript string. + +```js +const data = d3.csvParse(await FileAttachment("cars.csv").text()); +``` + +# FileAttachment(name).json() [<>](https://github.com/observablehq/stdlib/blob/master/src/fileAttachment.js "Source") + +Returns a promise to the file’s contents, parsed as JSON into JavaScript values. + +```js +const logs = await FileAttachment("weekend-logs.json").json(); +``` + +# FileAttachment(name).image() [<>](https://github.com/observablehq/stdlib/blob/master/src/fileAttachment.js "Source") + +Returns a promise to a file loaded as an [HTML Image](https://developer.mozilla.org/en-US/docs/Web/API/HTMLImageElement/Image). Note that the promise won't resolve until the image has finished loading — making this a useful value to pass to other cells that need to process the image, or paint it into a ``. + +```js +const image = await FileAttachment("sunset.jpg").image(); +``` + +# FileAttachment(name).arrayBuffer() [<>](https://github.com/observablehq/stdlib/blob/master/src/fileAttachment.js "Source") + +Returns a promise to the file’s contents as an [ArrayBuffer](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer). + +```js +const city = shapefile.read(await FileAttachment("sf.shp").arrayBuffer()); +``` + +# FileAttachment(name).stream() [<>](https://github.com/observablehq/stdlib/blob/master/src/fileAttachment.js "Source") + +Returns a promise to a [Stream](https://developer.mozilla.org/en-US/docs/Web/API/Streams_API) of the file’s contents. + +```js +const stream = await FileAttachment("metrics.csv").stream(); +const reader = stream.getReader(); +let done, value; +while (({done, value} = await reader.read()), !done) { + yield value; +} +``` + +# FileAttachment(name).blob() [<>](https://github.com/observablehq/stdlib/blob/master/src/fileAttachment.js "Source") + +Returns a promise to a [Blob](https://developer.mozilla.org/en-US/docs/Web/API/Blob) containing the raw contents of the file. + +```js +const blob = await FileAttachment("binary-data.dat").blob(); +``` + ### Generators # Generators.disposable(value, dispose) [<>](https://github.com/observablehq/stdlib/blob/master/src/generators/disposable.js "Source") diff --git a/package.json b/package.json index 72021df1..82eabc81 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,9 @@ { "name": "@observablehq/stdlib", - "version": "3.1.1", + "version": "3.2.0-rc.5", + "publishConfig": { + "tag": "next" + }, "license": "ISC", "main": "dist/stdlib.js", "module": "src/index.js", @@ -27,7 +30,7 @@ "dist/**/*.js" ], "dependencies": { - "d3-require": "^1.2.0", + "d3-require": "^1.2.4", "marked": "https://github.com/observablehq/marked.git#94c6b946f462fd25db4465d71a6859183f86c57f" }, "devDependencies": { diff --git a/src/dom/uid.js b/src/dom/uid.js index 4f8c14f8..639bc612 100644 --- a/src/dom/uid.js +++ b/src/dom/uid.js @@ -6,7 +6,7 @@ export default function(name) { function Id(id) { this.id = id; - this.href = window.location.href + "#" + id; + this.href = new URL(`#${id}`, location) + ""; } Id.prototype.toString = function() { diff --git a/src/fileAttachment.js b/src/fileAttachment.js new file mode 100644 index 00000000..4962b773 --- /dev/null +++ b/src/fileAttachment.js @@ -0,0 +1,50 @@ +async function remote_fetch(file) { + const response = await fetch(await file.url()); + if (!response.ok) throw new Error(`Unable to load file: ${file.name}`); + return response; +} + +class FileAttachment { + constructor(resolve, name) { + Object.defineProperties(this, { + _resolve: {value: resolve}, + name: {value: name, enumerable: true} + }); + } + async url() { + const url = await this._resolve(this.name); + if (url == null) throw new Error(`Unknown file: ${this.name}`); + return url; + } + async blob() { + return (await remote_fetch(this)).blob(); + } + async arrayBuffer() { + return (await remote_fetch(this)).arrayBuffer(); + } + async text() { + return (await remote_fetch(this)).text(); + } + async json() { + return (await remote_fetch(this)).json(); + } + async stream() { + return (await remote_fetch(this)).body; + } + async image() { + const url = await this.url(); + return new Promise((resolve, reject) => { + const i = new Image; + if (new URL(url, document.baseURI).origin !== new URL(location).origin) { + i.crossOrigin = "anonymous"; + } + i.onload = () => resolve(i); + i.onerror = () => reject(new Error(`Unable to load file: ${this.name}`)); + i.src = url; + }); + } +} + +export default function FileAttachments(resolve) { + return (name) => new FileAttachment(resolve, name); +} diff --git a/src/index.js b/src/index.js index 2897b3e8..fa926c0f 100644 --- a/src/index.js +++ b/src/index.js @@ -1 +1,2 @@ +export {default as FileAttachments} from "./fileAttachment"; export {default as Library} from "./library"; diff --git a/test/index-test.js b/test/index-test.js index 3c19fa2c..fe10264d 100644 --- a/test/index-test.js +++ b/test/index-test.js @@ -1,5 +1,5 @@ import { test } from "tap"; -import Library from "../src/library"; +import {Library, FileAttachments} from "../src"; test("new Library returns a library with the expected keys", async t => { t.deepEqual(Object.keys(new Library()).sort(), [ @@ -19,3 +19,9 @@ test("new Library returns a library with the expected keys", async t => { ]); t.end(); }); + +test("FileAttachments is exported by stdlib/index", t => { + t.equal(typeof FileAttachments, "function"); + t.equal(FileAttachments.name, "FileAttachments"); + t.end(); +}); diff --git a/yarn.lock b/yarn.lock index afb2cab6..d6211a74 100644 --- a/yarn.lock +++ b/yarn.lock @@ -818,10 +818,10 @@ csstype@^2.2.0: resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.6.tgz#c34f8226a94bbb10c32cc0d714afdf942291fc41" integrity sha512-RpFbQGUE74iyPgvr46U9t1xoQBM8T4BL8SxrN66Le2xYAPSaDJJKeztV3awugusb3g3G9iL8StmkBBXhcbbXhg== -d3-require@^1.2.0: - version "1.2.3" - resolved "https://registry.yarnpkg.com/d3-require/-/d3-require-1.2.3.tgz#65a1c2137e69c5c442f1ca2a04f72a3965124c16" - integrity sha512-zfat3WTZRZmh7jCrTIhg4zZvivA0DZvez8lZq0JwYG9aW8LcSUFO4eiPnL5F2MolHcLE8CLfEt06sPP8N7y3AQ== +d3-require@^1.2.4: + version "1.2.4" + resolved "https://registry.npmjs.org/d3-require/-/d3-require-1.2.4.tgz#59afc591d5089f99fecd8c45ef7539e1fee112b3" + integrity sha512-8UseEGCkBkBxIMouLMPONUBmU8DUPC1q12LARV1Lk/2Jwa32SVgmRfX8GdIeR06ZP+CG85YD3N13K2s14qCNyA== dashdash@^1.12.0: version "1.14.1"