rapier-embed 1.2.40 → 1.2.43

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/README.md CHANGED
@@ -20,7 +20,7 @@ Honor no-upload or no-host instructions. The workflow choices below are alternat
20
20
  Embed Rapier's Markdown editor or lightweight reader in your web app. Each build is one offline HTML file. The helper is an ES module: `import {Rapier} from 'rapier-embed'` or `<script type="module">`; there is no classic script or UMD build. Your
21
21
  app owns the document and storage; its agent edits live through WebMCP. The person sees the agent's presence and
22
22
  change marks, taps a change to see the before and undoes anything. Honor the person's Will/1 regions.
23
- The reader is one file of about 380 kB, `rapier.html` about 3.9 MB, with optional fonts and plug-ins. The helper is MIT with no dependencies. The two builds it frames are AGPL-3.0-only: use them under the AGPL, or
23
+ The reader is one file of about 400 kB, `rapier.html` about 4.0 MB, with optional fonts and plug-ins. The helper is MIT with no dependencies. The two builds it frames are AGPL-3.0-only: use them under the AGPL, or
24
24
  buy a commercial license for other terms (`LICENSING.md` in the Rapier repository).
25
25
 
26
26
  Choose one of two builds:
@@ -33,7 +33,7 @@ Choose one of two builds:
33
33
  Both render the same Markdown standard with the same layout. Download the reader from the root of the
34
34
  [rapier repository](https://github.com/jackskip22/rapier), or frame the hosted copies:
35
35
  `https://rapier.website/embed/rapier-reader.html` and `https://rapier.website/embed/rapier.html` (current). Both live under `/embed/`: `/rapier-reader.html` at the site root is a 404.
36
- Each release's own files stay at its tag: `https://cdn.jsdelivr.net/gh/jackskip22/rapier@v<version>/site/embed/<file>`. That address is for
36
+ Each release's own files stay at its tag: `https://cdn.jsdelivr.net/gh/jackskip22/rapier-plugins@cdn-v<version>/embed/<file>`. That address is for
37
37
  download only: jsDelivr serves the page as `text/plain` with `nosniff`, so a browser will not frame it. To pin a version, serve the
38
38
  downloaded copy yourself and pass it as `src`.
39
39
 
@@ -50,7 +50,7 @@ downloaded copy yourself and pass it as `src`.
50
50
  document access remain enforced.
51
51
 
52
52
  ```sh
53
- npm install rapier-embed@1.2.40
53
+ npm install rapier-embed@1.2.43
54
54
  ```
55
55
 
56
56
  Or copy the package's `embed.mjs` and `contract.mjs` into one directory of your app; no runtime CDN is needed.
@@ -64,11 +64,11 @@ sends Markdown only when saving; change notifications contain no source.
64
64
  Rapier is a phone-first Markdown editor for notes, diagrams, drawing and watercolor painting. It runs offline
65
65
  as one HTML file on Android, Web, Windows and Linux.
66
66
  Collaborate over MCP and WebMCP, use optional plain-file sync to your own cloud storage, or embed the
67
- editor and lightweight reader (about 370 kB). The agent edits live; the person sees its presence and
67
+ editor and lightweight reader (about 400 kB). The agent edits live; the person sees its presence and
68
68
  changes, taps a change to see the before and undoes anything. Use `show {target:"comparison"}` to show a diff deliberately.
69
69
 
70
70
  1. **Work together live.** Connect to `https://mcp.rapier.website/door/mcp`, then call `open` to share one document with the person.
71
- 2. **Deliver an offline editor.** Run `npx rapier-html@1.2.40 notes.md` to put the document and editor in one HTML file ([rapier-html](https://github.com/jackskip22/rapier-plugins/tree/main/npm/rapier-html)).
71
+ 2. **Deliver an offline editor.** Run `npx rapier-html@1.2.43 notes.md` to put the document and editor in one HTML file ([rapier-html](https://github.com/jackskip22/rapier-plugins/tree/main/npm/rapier-html)).
72
72
  3. **Embed in your app.** Install [rapier-embed](https://github.com/jackskip22/rapier-plugins/tree/main/npm/rapier-embed); mount the editor or reader, with your app's agent over WebMCP in the editor.
73
73
  4. **Keep portable Markdown.** Use [rapier-markdown-kit](https://github.com/jackskip22/rapier-plugins/tree/main/npm/rapier-markdown-kit) for pictures, editable SVG drawings and layout in one `.md` file. Respect [Will/1](https://github.com/jackskip22/will) `keep`, `append` and `edit` regions.
74
74
  5. **Encode JPEG XL.** Install [rapier-jxl](https://github.com/jackskip22/rapier-jxl) to encode pixels, photographs and existing JPEGs in JavaScript.
@@ -113,9 +113,22 @@ The frame needs the host page to be an `https` page, or `http` on `localhost`. `
113
113
  | `file://`, `app://` or another custom scheme | `host_scheme_unsupported` | The page has no origin the frame can answer. Serve the app over https, or http on localhost. |
114
114
  | `http://` on a LAN address or any host but `localhost` | `host_not_secure` | The hosted copies allow https and localhost parents only. Serve the app over https, or serve a copy of the page and set `src`. |
115
115
  | Cross-origin isolated (COEP `require-corp`) | `host_cross_origin_isolated` | The hosted copies carry no CORP or COEP header. Serve a copy with those headers and set `src`, or drop COEP. |
116
+ | A page with an opaque origin (a sandbox without `allow-same-origin`: an AI artifact, a playground preview) | `host_sandboxed` | The hosted copies refuse such a page. Give it an origin, or serve a copy of the page and set `src`. |
117
+ | An `http://` page around yours that is not `localhost` | `host_not_secure` | Every page around yours must be https (or localhost) for the hosted copies. |
118
+ | The browser is offline | `offline` | The hosted copies cannot be fetched. Mount again when the network is back, or serve a copy and set `src`. |
116
119
  | Your own `<iframe sandbox>` without `allow-scripts` or `allow-same-origin` | `sandbox_token_missing` | `mount` never widens a sandbox you wrote. Add the token, or pass a container and let `mount` create the iframe (it sets `allow-scripts allow-same-origin allow-downloads`). |
117
120
 
118
- A frame that loads but never connects, or never finishes loading, rejects `ready` and calls `onError` with `connect_timeout` after `timeout` (15 seconds). A page that does not allow your parent is the usual cause: the hosted pages send `frame-ancestors https: http://localhost:* http://127.0.0.1:*`, so a parent on `file://`, a custom scheme or plain http on a LAN address is blocked and never connects (`mount` names the first three at once, above). A copy you serve sets its own `frame-ancestors` (an HTTP response header; a `<meta>` tag cannot set it); list your parent's origin there. A plain `<iframe src=".../rapier.html">` with no helper opens an empty document when the parent shares the page's origin. From another origin it stays closed and says why. With `?embed=1` and no helper connected, the reader says "No document is loaded."
121
+ A frame that loads but never connects rejects `ready` and calls `onError` with `connect_timeout` once `timeout` (15 seconds) has passed since the frame's load event, so a slow download is not counted. A frame that never finishes loading rejects with `load_timeout` after 120 seconds. A frame that connects after `connect_timeout` still gets its document and its handle works (`connected` is true and the `connected` event fires); `ready` has already rejected, so wait for the event instead. A host whose own `frame-src` (or `default-src`) does not list the frame's origin blocks it the same way; the browser console names the policy. A page that does not allow your parent is the usual cause: the hosted pages send `frame-ancestors https: http://localhost:* http://127.0.0.1:*`, so a parent on `file://`, a custom scheme or plain http on a LAN address is blocked and never connects (`mount` names the first three at once, above). A copy you serve sets its own `frame-ancestors` (an HTTP response header; a `<meta>` tag cannot set it); list your parent's origin there. A plain `<iframe src=".../rapier.html">` with no helper opens an empty document when the parent shares the page's origin. From another origin it stays closed and says why. With `?embed=1` and no helper connected, the reader says "No document is loaded."
122
+
123
+ ### Keep typing when the tab closes
124
+
125
+ A reload, a route change and an unmount keep the person's typing for the next mount of the same document (see below). Closing the tab does not: its session storage goes with it. Ask the browser to confirm while something is unsaved:
126
+
127
+ ```js
128
+ let dirty = false;
129
+ const editor = Rapier.mount(element, {save, load, onState: state => { dirty = state.dirty; }});
130
+ addEventListener('beforeunload', event => { if (dirty) event.preventDefault(); });
131
+ ```
119
132
 
120
133
  ### Open a document by link
121
134
 
@@ -126,9 +139,9 @@ The reader on its own page opens a document at an address on its own origin: `ht
126
139
  A copy in your app works offline and changes only when you update it.
127
140
 
128
141
  1. Download the build you frame into a static directory, for example
129
- `https://cdn.jsdelivr.net/gh/jackskip22/rapier@v<version>/site/embed/rapier-reader.html` to `public/vendor/rapier/`.
142
+ `https://cdn.jsdelivr.net/gh/jackskip22/rapier-plugins@cdn-v<version>/embed/rapier-reader.html` to `public/vendor/rapier/`.
130
143
  2. Fetch the plug-ins your build uses into one directory beside it, from the manifest of the same version:
131
- `npx rapier-embed@1.2.40 plugins public/vendor/rapier/plugins --build reader`.
144
+ `npx rapier-embed@1.2.43 plugins public/vendor/rapier/plugins --build reader`.
132
145
  3. Mount with `src` and `plugins` pointing at your copies. Serve them over HTTPS (HTTP on localhost) and allow
133
146
  their origin in your `frame-src`. A same-origin parent can script its frame; use a dedicated origin when the
134
147
  frame must be isolated.
@@ -143,23 +156,23 @@ length and SHA-384 before it runs it, and keeps on the device. A file that fails
143
156
  <!-- PLUGIN_TABLE_BEGIN (tools/embed-plugin-table.mjs writes this table) -->
144
157
  | Plug-in | Adds | Size | Pages |
145
158
  | --- | --- | --- | --- |
146
- | `flowchart` | Draws Mermaid flowcharts and graphs natively, in the editor's own style. | 0.32 MB | The reader |
159
+ | `flowchart` | Draws Mermaid flowcharts and graphs natively, in the editor's own style. | 0.33 MB | The reader |
147
160
  | `docx` | Opens a Word document (.docx) as a Markdown document: headings, lists, tables, pictures and comments. | 0.61 MB | The reader |
148
161
  | `math` | Typesets TeX math ($...$ and $$...$$) as SVG. | 0.74 MB | Both |
149
- | `diagrams` | Draws every other Mermaid diagram: sequence, class, state, ER, Gantt, pie and more. | 2.57 MB (2 files) | Both |
162
+ | `diagrams` | Draws every other Mermaid diagram: sequence, class, state, ER, Gantt, pie and more. | 1.14 MB (2 files) | Both |
150
163
  | `pdf` | Opens a PDF as a Markdown document: its text layer, or each page as a picture. Fetches the pdf.js files itself. | 6.13 MB (198 files) | Both |
151
- | `ocr` | Reads the words in pictures on the device (PP-OCRv6 tiny on ONNX Runtime Web), so search finds them. About 21 MB. | 21.5 MB (10 files) | Both (the reader: only with `--with ocr`) |
164
+ | `ocr` | Reads the words in pictures on the device (PP-OCRv6 tiny on ONNX Runtime Web), so search finds them. About 21 MB. | 21.7 MB (10 files) | Both (the reader: only with `--with ocr`) |
152
165
  | `font-subset` | Keeps the used characters in fonts embedded in exported pages. | 1.21 MB | `rapier.html` |
153
166
  | `letters` | Draw's ornamental letter sets: Field, Relief, Leaf, Arabesque, Silhouette, Gothic, Sugar, Illuminated 1490, Tuscan 1845, Renaissance alphabet. | 1.31 MB (10 files) | `rapier.html` |
154
167
  | `templates` | Starters' traced page borders: 72 borders from the ornament book. | 6.03 MB (72 files) | `rapier.html` |
155
168
  | `elements` | The Elements sheet's traced ornaments: 3,155 pieces from the ornament book. | 16.2 MB (51 files) | `rapier.html` |
156
169
  | `lettersets` | The Gallery's letter sets: 31 sets of ornamental capitals. | 2.82 MB (31 files) | `rapier.html` |
157
170
 
158
- Totals: the reader, 10.4 MB (31.9 MB with `ocr`); `rapier.html`, 58.5 MB. Release 1.2.40.
171
+ Totals: the reader, 8.95 MB (30.6 MB with `ocr`); `rapier.html`, 57.2 MB. Release 1.2.43.
159
172
  <!-- PLUGIN_TABLE_END -->
160
173
 
161
174
  The manifest lists every plug-in file and, in `builds`, the pages that use it. `rapier.html` has flowcharts and Word import
162
- built in. `npx rapier-embed@1.2.40 plugins <directory> --build reader` leaves out `ocr` (text in pictures) unless you add `--with ocr`.
175
+ built in. `npx rapier-embed@1.2.43 plugins <directory> --build reader` leaves out `ocr` (text in pictures) unless you add `--with ocr`.
163
176
 
164
177
  **Approve plug-ins up front.** Without `plugins`, a frame asks its person "Install math plug-in?" the first time a document needs math or a diagram, and math and diagrams stay raw until they answer. A host that has decided passes `approvePlugins: true`: the frame installs the plug-in it needs without a question. Naming `plugins` has the same effect, because the host then serves the files itself. In a frame shorter than the question's sheet, the sheet's body scrolls.
165
178
 
@@ -172,7 +185,7 @@ The directory must be on the page's own origin, because the page's Content Secur
172
185
  throws a `TypeError` for a directory on another origin, and a page opened with one shows each plug-in as failed, with the
173
186
  reason, without making a request. To keep the plug-ins on a CDN, serve the page from the same origin.
174
187
 
175
- `npx rapier-embed@1.2.40 plugins <directory>` fetches every file the manifest lists, checks it, and writes it. It
188
+ `npx rapier-embed@1.2.43 plugins <directory>` fetches every file the manifest lists, checks it, and writes it. It
176
189
  keeps a file that is already there and correct, and exits with status 1 if any file is missing or fails its check.
177
190
  `--build reader` or `--build full` keeps the files that page uses, `--only name,name` fetches some
178
191
  plug-ins, `--check` verifies a directory without fetching, `--from <address>` fetches from your own mirror, and
@@ -184,7 +197,7 @@ manifest must be present unless you supply one. Only regular files are accepted
184
197
  symbolic links in plug-in paths are refused. Keep the directory under your control while the command runs.
185
198
 
186
199
  The manifest, `rapier-plugins.json`, ships in the package and stands beside each published reader at
187
- `https://cdn.jsdelivr.net/gh/jackskip22/rapier@v<version>/site/embed/rapier-plugins.json`. Use the manifest of the same version as your
200
+ `https://cdn.jsdelivr.net/gh/jackskip22/rapier-plugins@cdn-v<version>/embed/rapier-plugins.json`. Use the manifest of the same version as your
188
201
  `rapier-reader.html`: each reader pins the files it was built with. It names every plug-in, what it adds, and each
189
202
  file's `file` (a path: the PDF reader's pdf.js files keep their folders), `bytes`, `sha384` (hex), `sri` (base64)
190
203
  and source `url`. Without Node, fetch and check the files like this:
@@ -378,15 +391,16 @@ apply the old insertion to the replacement document. Asset failures do not rejec
378
391
  | `plugins` | The directory that holds the page's plug-in files, on the page's own origin (a relative address is read from your page); the page then fetches from nowhere else. Either build |
379
392
  | `approvePlugins` | `true`: install math and diagram plug-ins without asking the person. Naming `plugins` does the same. Default `false` |
380
393
  | `title`, `onError(error)` | The iframe's accessible title, and an error listener |
381
- | `timeout` | Milliseconds a loaded frame has to connect before `ready` rejects with `connect_timeout`; default 15000 |
394
+ | `timeout` | Milliseconds after the frame loads that it has to connect before `ready` rejects with `connect_timeout`; default 15000 |
382
395
 
383
396
  The handle has `iframe`, `ready` (a promise) and `connected`. `load(content, {revision, filename?, readOnly?,
384
397
  title?})`, `save()`, `compare(content, {filename?})` and `close()` use their grants. `theme(value)` changes the
385
398
  host theme; `style({css, fonts})` restyles the reader; `disconnect()` ends the connection, rejects unfinished
386
399
  operations and removes the iframe that mount created in a container (an iframe you passed stays). Mount does not size
387
400
  a frame it creates: give it a size in your CSS, for example `#editor iframe {width: 100%; height: 100%; border: 0}`.
388
- Tab walks every control and link in the frame. Escape, with nothing open in the frame and no text field focused, moves
389
- focus to the next control on your page after the frame; Shift+Escape moves it to the control before.
401
+ Mount adds the frame after what the container already holds: clear a server-rendered placeholder first (`container.replaceChildren()`). The handle lives as long as its iframe is in the page: if a framework removes it (`innerHTML`, `remove()`) without `disconnect()`, `connected` turns false, an unfinished `save()` rejects, and later calls reject `frame_removed`. The person's typing stays as a draft for the next mount of the same document.
402
+ Tab walks every control and link in the frame, except inside the editor text, where Tab indents. Escape, with nothing open in the frame and no text field focused, moves
403
+ focus to the next control on your page after the frame; Shift+Escape moves it to the control before. From the editor text the way out is Escape, then Tab (Shift+Tab goes back).
390
404
  `on('connected' | 'state' | 'close-request' | 'closed' | 'error', fn)` returns an
391
405
  unsubscribe function. State is `{loaded, dirty, saving, closing, readOnly, filename, docKind}`. Mount a fresh frame to
392
406
  change grants or settings. A denied or dirty replacement load rejects.
@@ -394,9 +408,11 @@ change grants or settings. A denied or dirty replacement load rejects.
394
408
  Every refusal is an `Error` with a `code`. A connect the frame refuses rejects `ready`: `contract_unsupported`,
395
409
  `capabilities_invalid`, a `settings_*` code, or `invalid_payload` with a `reason`. A frame that cannot start rejects
396
410
  `ready` and calls `onError`: `boot_failed` when the page shows its startup error, `connect_timeout` when it does not connect
397
- within `timeout` (a page that cannot load, such as a 404, ends here). `load()` rejects with `document_unshowable` when the
398
- reader cannot show the document; the reader keeps the document it showed. It rejects with `revision_stale` for a
399
- revision older than the frame's and `revision_conflict` for the same revision with other content. `mount` throws a
411
+ within `timeout` of loading, `load_timeout` when it does not load in 120 seconds. `load()` rejects with `load_timeout` when the frame
412
+ does not answer within 120 seconds, `load_busy` while an earlier `load()` is still being shown, `invalid_payload` for a filename or
413
+ document it refuses, and `document_unshowable` when the reader cannot show the document; the reader keeps the document it showed. A
414
+ document over 1 MB resolves `load()` when its first screen shows; the reader draws the rest after it. It rejects with `revision_stale` for a
415
+ revision older than the frame's and `revision_conflict` for the same revision with other content. It rejects with `document_open_elsewhere` when another editor already has the same `sessionId` and `documentId` open (on mount, `ready` rejects with it); the first editor keeps its typing. `mount` throws a
400
416
  `TypeError` with a code for an unknown option (`option_unknown`), a build whose `src` names the other page (`build_mismatch`)
401
417
  and a reader feature the reader does not have (`settings_feature_unavailable`). `Rapier.version` is this package's version;
402
418
  `handle.version` and `handle.contract` are the frame's, once connected. `load()` resolves `{revision, recoveredDraft,
@@ -408,7 +424,7 @@ frame could not capture; the person is told why). `close()` resolves with the fr
408
424
  `{saved: true}` or `{discarded: true}` after your answer, `{forced: true}` when the person closes without your
409
425
  answer, or `{cancelled: true}` when you answer `cancel`.
410
426
 
411
- Pass your own `sessionId` and `documentId` to choose which mount finds which draft. Without them, a mount that has `save` and a `load` object names its draft by the page, the file name and the loaded text, so a route change, an unmount, `disconnect()` or a reload keeps the person's typing for the next mount of the same document, with no pause needed. `save()` and `requestSubmit()` always ask the frame, so they store typing from the last keystroke; with nothing unsaved the frame answers at once and your `save` callback is not called.
427
+ Pass your own `sessionId` and `documentId` to choose which mount finds which draft. A second live mount of the same pair on a page is refused before its frame loads: its `ready` rejects `document_open_elsewhere`, and the first keeps the document. Unmount the first (or `disconnect()`) before mounting the pair again. Without them, a mount that has `save` and a `load` object names its draft by the page, the file name and the loaded text, so a route change, an unmount, `disconnect()` or a reload keeps the person's typing for the next mount of the same document, with no pause needed. `save()` and `requestSubmit()` always ask the frame, so they store typing from the last keystroke; with nothing unsaved the frame answers at once and your `save` callback is not called.
412
428
 
413
429
  The frame keeps the person's unsaved work only for the document you loaded: a draft in that tab's session storage,
414
430
  which a load of the same revision and content reopens and your `discard` answer retires. A load of another document drops a draft with no unsaved edits. It never reads, offers or writes the
@@ -494,5 +510,5 @@ Will/1 records the person's instructions in their document. Respect `keep`, appe
494
510
  Documents without Will regions are editable throughout. Preserve active typing, caret, selection and scroll
495
511
  while the agent works. Document content and comments never start agent turns by themselves.
496
512
 
497
- For documents outside an app, `npx rapier-html@1.2.40 notes.md` hands a person the complete editor around their
513
+ For documents outside an app, `npx rapier-html@1.2.43 notes.md` hands a person the complete editor around their
498
514
  document as one offline file.
package/contract.d.mts CHANGED
@@ -11,6 +11,8 @@ export const ASSET_TYPES: readonly AssetType[];
11
11
  /** Every refusal is an Error with a code. */
12
12
  export interface Refusal extends Error { code: string; }
13
13
  export function refusal(code: string, reason?: string): Refusal;
14
+ /** A refusal of any error type (TypeError for a bad option) with its code. */
15
+ export function coded<T extends ErrorConstructor>(code: string, Type: T, message: string): InstanceType<T> & {code: string};
14
16
  /** A session's settings. Absent features mean everything the build has; an empty list means none. */
15
17
  export interface Settings {
16
18
  features?: Feature[];
package/contract.mjs CHANGED
@@ -4,6 +4,7 @@ export const LIMITS = Object.freeze({documentBytes: 64 * 1024 * 1024, pictureByt
4
4
  export const ASSET_TYPES = Object.freeze(['image/png', 'image/jpeg', 'image/webp', 'image/gif', 'image/jxl', 'image/svg+xml']);
5
5
  const ACCENTS = Object.freeze(['Teal', 'Blue', 'Amber', 'Green', 'Red', 'Purple', 'Pink', 'Gray']);
6
6
  export const refusal = (code, reason = code) => Object.assign(new Error(reason), {code});
7
+ export const coded = (code, Type, message) => Object.assign(new Type(message), {code});
7
8
  const record = value => value !== null && typeof value === 'object' && !Array.isArray(value) &&
8
9
  (Object.getPrototypeOf(value) === null || Object.getPrototypeOf(Object.getPrototypeOf(value)) === null);
9
10
  const only = (value, keys) => Object.keys(value).every(key => keys.includes(key));
package/embed.d.mts CHANGED
@@ -59,7 +59,7 @@ export interface MountOptions {
59
59
  approvePlugins?: boolean;
60
60
  /** The iframe's accessible title. */
61
61
  title?: string;
62
- /** Milliseconds a loaded frame has to connect before `ready` rejects with `connect_timeout`; default 15000. */
62
+ /** Milliseconds after the frame loads that it has to connect before `ready` rejects with `connect_timeout`; default 15000. A frame that never loads rejects with `load_timeout` after 120 seconds. */
63
63
  timeout?: number;
64
64
  onError?(error: Refusal | Error): void;
65
65
  }
@@ -83,7 +83,7 @@ export interface MountedEditor {
83
83
  /** Shows another document. `revision` is optional here: omitted, the helper uses one above the last revision it saw, or 1.
84
84
  * Rejects with `revision_stale` for a revision older than the frame's and `revision_conflict` for the same revision with
85
85
  * other content (both for the same filename only: another filename is another document), with `unsaved_changes` while
86
- * the person has unsaved edits, `draft_retained` while unsaved edits from another revision are kept, and
86
+ * the person has unsaved edits, `document_open_elsewhere` when another editor has the same ids open, `draft_retained` while unsaved edits from another revision are kept, and
87
87
  * `document_unshowable` when the reader cannot show the document. */
88
88
  load(content: string, options?: {revision?: Revision; filename?: string; readOnly?: boolean; title?: string; baseUrl?: string}): Promise<LoadResult>;
89
89
  /** The same, with the record `mount` takes: `load({content, revision, filename})`. */