rapier-embed 1.2.45 → 1.2.48
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 +111 -53
- package/contract.mjs +16 -3
- package/embed.d.mts +14 -6
- package/embed.mjs +262 -91
- package/package.json +1 -1
- package/rapier-plugins.json +200 -200
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
|
|
23
|
+
The reader is one file of about 410 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:
|
|
@@ -44,13 +44,13 @@ downloaded copy yourself and pass it as `src`.
|
|
|
44
44
|
- The reader changes nothing. Restyle it with CSS custom properties and your own fonts; keep it and its plug-ins
|
|
45
45
|
on your own origin.
|
|
46
46
|
- Editor saves are revision-checked: a conflict keeps the person's source in the editor.
|
|
47
|
-
- `<rapier-editor>` is a form field whose value is the exact
|
|
47
|
+
- `<rapier-editor>` is a form field whose value is the exact captured Markdown, pictures and line endings included.
|
|
48
48
|
- With `agent: true`, your app's agent works beside the person or while they are away. Rapier shows what changed;
|
|
49
49
|
`show {target:"comparison"}` lets the agent display a diff deliberately. Currentness, source scope and authenticated
|
|
50
50
|
document access remain enforced.
|
|
51
51
|
|
|
52
52
|
```sh
|
|
53
|
-
npm install rapier-embed@1.2.
|
|
53
|
+
npm install rapier-embed@1.2.48
|
|
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
|
|
67
|
+
editor and lightweight reader (about 410 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.**
|
|
71
|
+
2. **Deliver an offline editor.** `export {document, format:"html"}` puts the document and editor in one HTML file; on their own computer, a person can make one with the [rapier-html](https://github.com/jackskip22/rapier-plugins/tree/main/npm/rapier-html) package.
|
|
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.
|
|
@@ -118,11 +118,11 @@ The frame needs the host page to be an `https` page, or `http` on `localhost`. `
|
|
|
118
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`. |
|
|
119
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`). |
|
|
120
120
|
|
|
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.
|
|
121
|
+
A frame that loads but never connects rejects `ready` and calls `onError` with `connect_timeout` once `timeout` (15 seconds by default) 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 by default. After connection, the first host loader and the frame's load acknowledgment together have the same 120-second bound; later `load()` calls use that acknowledgment bound too. A `timeout` greater than 120,000 milliseconds raises those load bounds. A late connection can still become live and load the document: `connected` becomes true and its event fires, but an already rejected `ready` stays rejected. After a first-load failure, a connected handle can retry with `load()`; old loader results cannot replace the accepted retry. 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
122
|
|
|
123
123
|
### Keep typing when the tab closes
|
|
124
124
|
|
|
125
|
-
|
|
125
|
+
Draft recovery needs the same identity, saved revision and content in the same tab (see Draft identity below). Closing the tab ends its session storage. Ask the browser to confirm while something is unsaved:
|
|
126
126
|
|
|
127
127
|
```js
|
|
128
128
|
let dirty = false;
|
|
@@ -136,17 +136,21 @@ The reader on its own page opens a document at an address on its own origin: `ht
|
|
|
136
136
|
|
|
137
137
|
## Keep Rapier in your codebase
|
|
138
138
|
|
|
139
|
-
|
|
139
|
+
Keep the helper, page and plug-in manifest on one version. For version 1.2.46:
|
|
140
140
|
|
|
141
|
-
1.
|
|
142
|
-
|
|
143
|
-
2.
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
141
|
+
1. Install the helper: `npm install rapier-embed@1.2.48`.
|
|
142
|
+
2. Create `public/vendor/rapier/` and save the reader into it from
|
|
143
|
+
`https://cdn.jsdelivr.net/gh/jackskip22/rapier-plugins@cdn-v1.2.46/embed/rapier-reader.html`.
|
|
144
|
+
3. Fetch its plug-ins from the same version's manifest:
|
|
145
|
+
`node cli.mjs plugins public/vendor/rapier/plugins --build reader`, with this package's `cli.mjs` (in the Claude
|
|
146
|
+
plugin, the `cli.mjs` beside this skill). It does not fetch the page.
|
|
147
|
+
4. Use the complete mount in Show a document above, with `src`, `plugins` and `load`. Serve the files over HTTPS
|
|
148
|
+
(HTTP on localhost) and allow their origin in your `frame-src`. Give the container a height. A same-origin
|
|
149
|
+
parent can script its frame; use a dedicated origin when the frame must be isolated.
|
|
148
150
|
|
|
149
|
-
|
|
151
|
+
For the editor, download `rapier.html` from the same tag and use `--build full`. To update, replace the helper,
|
|
152
|
+
page and plug-ins together. Serving your own files avoids a runtime CDN dependency; offline navigation still
|
|
153
|
+
needs the host to make those files available.
|
|
150
154
|
|
|
151
155
|
## Plug-ins
|
|
152
156
|
|
|
@@ -156,11 +160,11 @@ length and SHA-384 before it runs it, and keeps on the device. A file that fails
|
|
|
156
160
|
<!-- PLUGIN_TABLE_BEGIN (tools/embed-plugin-table.mjs writes this table) -->
|
|
157
161
|
| Plug-in | Adds | Size | Pages |
|
|
158
162
|
| --- | --- | --- | --- |
|
|
159
|
-
| `flowchart` | Draws Mermaid flowcharts and graphs natively, in the editor's own style. | 0.
|
|
160
|
-
| `docx` | Opens a Word document (.docx) as a Markdown document: headings, lists, tables, pictures and comments. | 0.
|
|
163
|
+
| `flowchart` | Draws Mermaid flowcharts and graphs natively, in the editor's own style. | 0.34 MB | The reader |
|
|
164
|
+
| `docx` | Opens a Word document (.docx) as a Markdown document: headings, lists, tables, pictures and comments. | 0.91 MB | The reader |
|
|
161
165
|
| `math` | Typesets TeX math ($...$ and $$...$$) as SVG. | 0.74 MB | Both |
|
|
162
166
|
| `diagrams` | Draws every other Mermaid diagram: sequence, class, state, ER, Gantt, pie and more. | 1.14 MB (2 files) | Both |
|
|
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.
|
|
167
|
+
| `pdf` | Opens a PDF as a Markdown document: its text layer, or each page as a picture. Fetches the pdf.js files itself. | 6.15 MB (198 files) | Both |
|
|
164
168
|
| `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`) |
|
|
165
169
|
| `font-subset` | Keeps the used characters in fonts embedded in exported pages. | 1.21 MB | `rapier.html` |
|
|
166
170
|
| `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` |
|
|
@@ -168,11 +172,11 @@ length and SHA-384 before it runs it, and keeps on the device. A file that fails
|
|
|
168
172
|
| `elements` | The Elements sheet's traced ornaments: 3,155 pieces from the ornament book. | 16.2 MB (51 files) | `rapier.html` |
|
|
169
173
|
| `lettersets` | The Gallery's letter sets: 31 sets of ornamental capitals. | 2.82 MB (31 files) | `rapier.html` |
|
|
170
174
|
|
|
171
|
-
Totals: the reader,
|
|
175
|
+
Totals: the reader, 9.29 MB (30.9 MB with `ocr`); `rapier.html`, 57.2 MB. Release 1.2.48.
|
|
172
176
|
<!-- PLUGIN_TABLE_END -->
|
|
173
177
|
|
|
174
178
|
The manifest lists every plug-in file and, in `builds`, the pages that use it. `rapier.html` has flowcharts and Word import
|
|
175
|
-
built in. `
|
|
179
|
+
built in. `node cli.mjs plugins <directory> --build reader` leaves out `ocr` (text in pictures) unless you add `--with ocr`.
|
|
176
180
|
|
|
177
181
|
**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.
|
|
178
182
|
|
|
@@ -185,7 +189,7 @@ The directory must be on the page's own origin, because the page's Content Secur
|
|
|
185
189
|
throws a `TypeError` for a directory on another origin, and a page opened with one shows each plug-in as failed, with the
|
|
186
190
|
reason, without making a request. To keep the plug-ins on a CDN, serve the page from the same origin.
|
|
187
191
|
|
|
188
|
-
`
|
|
192
|
+
`node cli.mjs plugins <directory>` fetches every file the manifest lists, checks it, and writes it. It
|
|
189
193
|
keeps a file that is already there and correct, and exits with status 1 if any file is missing or fails its check.
|
|
190
194
|
`--build reader` or `--build full` keeps the files that page uses, `--only name,name` fetches some
|
|
191
195
|
plug-ins, `--check` verifies a directory without fetching, `--from <address>` fetches from your own mirror, and
|
|
@@ -200,15 +204,8 @@ The manifest, `rapier-plugins.json`, ships in the package and stands beside each
|
|
|
200
204
|
`https://cdn.jsdelivr.net/gh/jackskip22/rapier-plugins@cdn-v<version>/embed/rapier-plugins.json`. Use the manifest of the same version as your
|
|
201
205
|
`rapier-reader.html`: each reader pins the files it was built with. It names every plug-in, what it adds, and each
|
|
202
206
|
file's `file` (a path: the PDF reader's pdf.js files keep their folders), `bytes`, `sha384` (hex), `sri` (base64)
|
|
203
|
-
and source `url`. Without Node,
|
|
204
|
-
|
|
205
|
-
```sh
|
|
206
|
-
jq -r --arg build reader '.plugins[] | .builds as $b | .files[] | select((.builds // $b) | index($build)) | "\(.url) \(.file) \(.sha384)"' rapier-plugins.json | while read -r url file sum; do
|
|
207
|
-
mkdir -p "public/vendor/rapier/plugins/$(dirname "$file")"
|
|
208
|
-
curl -fsSL "$url" -o "public/vendor/rapier/plugins/$file" &&
|
|
209
|
-
echo "$sum public/vendor/rapier/plugins/$file" | sha384sum -c -
|
|
210
|
-
done
|
|
211
|
-
```
|
|
207
|
+
and source `url`. Without Node, save each file from its `url` to its `file` path in your plug-in folder and check
|
|
208
|
+
it against its `sha384`; for the editor, take the files whose `builds` include `full`.
|
|
212
209
|
|
|
213
210
|
**Without `plugins`.** `rapier.html` looks for the `math` and `diagrams` files beside the page,
|
|
214
211
|
at the file names the manifest lists, before their pinned addresses. The
|
|
@@ -302,14 +299,15 @@ Mount takes a container or an iframe and owns its navigation, title and clipboar
|
|
|
302
299
|
navigation, and accepts readiness only from that iframe's window and exact origin. HTTPS is required, except HTTP
|
|
303
300
|
on localhost.
|
|
304
301
|
|
|
305
|
-
`load` is a record or an async callback returning `{content, revision, filename?, readOnly?, title?, baseUrl?}`.
|
|
306
|
-
callback
|
|
307
|
-
|
|
308
|
-
|
|
302
|
+
`load` is a record or an async callback returning `{content, revision, filename?, readOnly?, title?, baseUrl?}`.
|
|
303
|
+
Mount snapshots a record; a callback is called again after each connection to request the host's current record.
|
|
304
|
+
The `save` callback receives `{content, filename, docKind, codeLang, requestId, baseRevision}` and returns
|
|
305
|
+
`{revision}` directly or through a promise, only after durable storage confirms the write. A repeated request ID
|
|
306
|
+
shares the first write and repeats its first answer, even while that write is pending. A repeated ID with different source, metadata or base revision is refused as
|
|
309
307
|
`save_request_conflict`. Source must fit the session's UTF-8 document byte limit before it reaches your callback.
|
|
310
308
|
Oversized source is `document_too_large`; ill-formed Unicode and malformed metadata are `save_payload_invalid`.
|
|
311
309
|
The helper holds answers for its lifetime; your store must also deduplicate IDs across host
|
|
312
|
-
reloads.
|
|
310
|
+
reloads. Choose the draft identity described below when you intend to recover a session.
|
|
313
311
|
|
|
314
312
|
Switch documents with `editor.load(content, {revision, filename})` or `editor.load({content, revision, filename})`. `revision` is optional in `load()`: left out, the helper uses one above the last revision it saw, or 1. `mount`'s `load` needs it. A load always shows and saves the document you gave it. It is refused by code while the person has unsaved edits (`unsaved_changes`; call `save()` first), and for the same filename with a revision older than the frame's (`revision_stale`) or with the same revision and other content (`revision_conflict`). Another filename is another document with its own revisions.
|
|
315
313
|
|
|
@@ -379,6 +377,7 @@ apply the old insertion to the replacement document. Asset failures do not rejec
|
|
|
379
377
|
| --- | --- |
|
|
380
378
|
| `build` | `'full'` (default; `rapier.html`, also named `'document'`) or `'reader'` |
|
|
381
379
|
| `src` | Your copy of the page; omitted, the hosted current build |
|
|
380
|
+
| `sessionId`, `documentId` | The fixed draft identity for this handle; see Draft identity below |
|
|
382
381
|
| `load` | Grants `open` |
|
|
383
382
|
| `save` | Grants `read`, plus `changes` to observe save completion |
|
|
384
383
|
| `onState(state)` | Grants `changes` |
|
|
@@ -394,11 +393,37 @@ apply the old insertion to the replacement document. Asset failures do not rejec
|
|
|
394
393
|
| `timeout` | Milliseconds after the frame loads that it has to connect before `ready` rejects with `connect_timeout`; default 15000 |
|
|
395
394
|
|
|
396
395
|
The handle has `iframe`, `ready` (a promise) and `connected`. `load(content, {revision, filename?, readOnly?,
|
|
397
|
-
title?})`, `save()`, `compare(content, {filename?})` and `close()` use their grants. `theme(value)` changes
|
|
398
|
-
host theme; `style({css, fonts})` restyles the reader
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
396
|
+
title?, baseUrl?})`, `save()`, `compare(content, {filename?})` and `close()` use their grants. `theme(value)` changes
|
|
397
|
+
the host theme; `style({css, fonts})` restyles the reader. `ready` resolves after connection and the initial load,
|
|
398
|
+
when supplied. The `connected` event identifies a live connection; it does not mean that the document has loaded.
|
|
399
|
+
The original `ready` promise is not replaced on reconnect.
|
|
400
|
+
|
|
401
|
+
A created iframe has 100% width and height and no border; your CSS can override those attributes. Give its
|
|
402
|
+
container a height. Mount adds the frame after what the container already holds: clear a server-rendered
|
|
403
|
+
placeholder first (`container.replaceChildren()`).
|
|
404
|
+
|
|
405
|
+
### Lifecycle
|
|
406
|
+
|
|
407
|
+
| Transition | What happens |
|
|
408
|
+
| --- | --- |
|
|
409
|
+
| Mount | Fix the IDs, grants and settings before navigation. Snapshot an initial load record, or call its loader after connection. |
|
|
410
|
+
| Accepted load | Keep the complete record: content, revision, filename, readOnly, title and baseUrl. A refused load leaves the last accepted record intact. |
|
|
411
|
+
| Confirmed Save | Keep the identity, update the saved content and revision, and wait for the frame to hold no newer edits before `save()` resolves. |
|
|
412
|
+
| Iframe navigation or reload | Reject old pending operations. Replay the last accepted record, updated by confirmed saves, when `load` was an object; call a function loader again for the current record. |
|
|
413
|
+
| Port error | Retire the connection and reject pending calls with `disconnected`. Recovery waits for a later iframe load. |
|
|
414
|
+
| Move within one task | Keep the connection if the iframe is attached when removal is observed. |
|
|
415
|
+
| Observed removal | Set `connected` to false, reject pending calls with `frame_removed` and release the active ID pair. The handle keeps its recovery state and iframe load listener. |
|
|
416
|
+
| Reinsert the same iframe | Reconnect that handle if it was not explicitly disconnected and the ID pair is free. If another writer owns the pair, refuse with `document_open_elsewhere`. Retired operations do not resume. |
|
|
417
|
+
| `disconnect()` | End the handle permanently, even while detached; reject unfinished operations and release ports, listeners, observers and timers. Remove an iframe mount created; leave an iframe you supplied. A new mount gets a new handle. |
|
|
418
|
+
|
|
419
|
+
Removal, reconnect and `disconnect()` do not save or discard the document. There is no autosave on unmount. A
|
|
420
|
+
late callback from an old connection cannot acknowledge work on its replacement; storage it already performed
|
|
421
|
+
remains your application's responsibility. A matching draft can be recovered as described below.
|
|
422
|
+
|
|
423
|
+
An editor iframe you supplied keeps its loaded document and its claim after disconnection. Another frame with
|
|
424
|
+
the same ID pair remains refused while that document is open. Remove or navigate the supplied iframe before
|
|
425
|
+
another frame opens the pair. Ending the transport does not discard the retained unsaved words.
|
|
426
|
+
|
|
402
427
|
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
428
|
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).
|
|
404
429
|
`on('connected' | 'state' | 'close-request' | 'closed' | 'error', fn)` returns an
|
|
@@ -408,10 +433,11 @@ change grants or settings. A denied or dirty replacement load rejects.
|
|
|
408
433
|
Every refusal is an `Error` with a `code`. A connect the frame refuses rejects `ready`: `contract_unsupported`,
|
|
409
434
|
`capabilities_invalid`, a `settings_*` code, or `invalid_payload` with a `reason`. A frame that cannot start rejects
|
|
410
435
|
`ready` and calls `onError`: `boot_failed` when the page shows its startup error, `connect_timeout` when it does not connect
|
|
411
|
-
within `timeout` of loading, `load_timeout` when
|
|
412
|
-
does not answer within
|
|
413
|
-
|
|
414
|
-
|
|
436
|
+
within `timeout` of loading, or `load_timeout` when the page or first document exceeds its load bound. `load()`
|
|
437
|
+
rejects with `load_timeout` when the frame does not answer within that bound, `load_busy` while an earlier load is
|
|
438
|
+
still being shown, `invalid_payload` for malformed input, `invalid_filename` or `invalid_base_url` for those
|
|
439
|
+
fields, `load_failed` when the frame cannot finish loading, and `document_unshowable` when the reader cannot
|
|
440
|
+
show the document; the reader keeps the document it showed. A 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
441
|
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
|
|
416
442
|
`TypeError` with a code for an unknown option (`option_unknown`), a build whose `src` names the other page (`build_mismatch`)
|
|
417
443
|
and a reader feature the reader does not have (`settings_feature_unavailable`). `Rapier.version` is this package's version;
|
|
@@ -424,7 +450,24 @@ frame could not capture; the person is told why). `close()` resolves with the fr
|
|
|
424
450
|
`{saved: true}` or `{discarded: true}` after your answer, `{forced: true}` when the person closes without your
|
|
425
451
|
answer, or `{cancelled: true}` when you answer `cancel`.
|
|
426
452
|
|
|
427
|
-
|
|
453
|
+
### Draft identity
|
|
454
|
+
|
|
455
|
+
| Mount options | Identity |
|
|
456
|
+
| --- | --- |
|
|
457
|
+
| Both `sessionId` and `documentId` | The exact pair you supply. |
|
|
458
|
+
| `documentId` and `save`, with no `sessionId` | Your document ID and a stable session derived from the host's origin and path. |
|
|
459
|
+
| Neither ID, with `save` and a `load` object | An automatic pair derived from the host page, filename, loaded text and an available matching-document slot. |
|
|
460
|
+
| Other omitted IDs | Generated IDs; provide your own stable pair for recovery across new mounts. |
|
|
461
|
+
|
|
462
|
+
The chosen IDs stay fixed for the handle. Changing filename changes the document's revision rules, not those
|
|
463
|
+
IDs. A second live mount of the same pair on a page is refused before its frame loads: `ready` rejects with
|
|
464
|
+
`document_open_elsewhere`, and the first keeps its document. Retire the first frame's document before another
|
|
465
|
+
frame acquires that pair; disconnect removes a helper-created frame, while a supplied frame needs removal or
|
|
466
|
+
navigation. Recovery after a route change, unmount or reload needs the same IDs, saved revision
|
|
467
|
+
and content in the same tab. Form replacements have the further limit described below.
|
|
468
|
+
|
|
469
|
+
`save()` and `requestSubmit()` ask the frame to capture its current text. With nothing unsaved, the frame answers
|
|
470
|
+
at once and the save callback is not called.
|
|
428
471
|
|
|
429
472
|
The frame keeps the person's unsaved work only for the document you loaded: a draft in that tab's session storage,
|
|
430
473
|
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
|
|
@@ -452,9 +495,10 @@ Write here.</textarea>
|
|
|
452
495
|
</script>
|
|
453
496
|
```
|
|
454
497
|
|
|
455
|
-
The textarea works without script. After upgrade, `element.value` is the exact
|
|
456
|
-
|
|
457
|
-
|
|
498
|
+
The textarea works without script. After upgrade, `element.value` is the exact captured Markdown;
|
|
499
|
+
`await element.save()` captures the current source and returns that string. `change` fires when captured text
|
|
500
|
+
updates the field. Submit captures the edited source first, then continues native form validation and
|
|
501
|
+
submission with its submitter. Use `requestSubmit()` for scripted submissions; `form.submit()` bypasses submit events and cannot
|
|
458
502
|
capture pending edits.
|
|
459
503
|
|
|
460
504
|
The successful field is a `text/markdown` file part named `body`: read its uploaded bytes on the server, or
|
|
@@ -462,9 +506,23 @@ The successful field is a `text/markdown` file part named `body`: read its uploa
|
|
|
462
506
|
exactly; native text parts normalize line endings. The restoration state and `value` remain strings. Pictures
|
|
463
507
|
already live in the Markdown, so use `multipart/form-data`.
|
|
464
508
|
|
|
465
|
-
Reset restores the initial text
|
|
466
|
-
|
|
467
|
-
|
|
509
|
+
Reset restores the initial text. Assigning `value` selects that exact string; when the browser supplies a string
|
|
510
|
+
for form state restoration, the field restores it exactly. Each replacement retires pending field work and
|
|
511
|
+
selects fresh draft IDs held in the element's memory. A late capture cannot overwrite the replacement or resume
|
|
512
|
+
an old form submission.
|
|
513
|
+
|
|
514
|
+
On its first mount, a field with a DOM `id` derives its identity from the host page, initial filename and text,
|
|
515
|
+
and that ID. A field without an ID uses the automatic matching-document slot rule. Both kinds keep their chosen
|
|
516
|
+
IDs across removal and reinsertion of the same element. The fresh IDs selected by Reset, value assignment or
|
|
517
|
+
state restoration are also kept across that same-element remount, so typing after the replacement can recover.
|
|
518
|
+
|
|
519
|
+
A full host-page reload creates a new element and loses the replacement's in-memory IDs. The host or browser
|
|
520
|
+
must restore its replacement value. Recreating the original automatic identity can reopen a pre-Reset draft;
|
|
521
|
+
it cannot find later typing kept under the replacement's generated IDs. The helper does not erase that earlier
|
|
522
|
+
session-storage draft or persist the replacement identity in another store.
|
|
523
|
+
|
|
524
|
+
`required`, `disabled`, labels, focus and native validity methods work as a field's do. Disabling first keeps
|
|
525
|
+
any pending edits, then makes the editor read-only and excludes the field from submission. A phone opens its text preview in a
|
|
468
526
|
full-screen editor; a wide screen edits inline. Resizing keeps the same editor session. Listen for `error` to
|
|
469
527
|
report a refused capture without submitting stale text. Form saves hold the source in the field until
|
|
470
528
|
submission; they are not durable server saves.
|
|
@@ -510,5 +568,5 @@ Will/1 records the person's instructions in their document. Respect `keep`, appe
|
|
|
510
568
|
Documents without Will regions are editable throughout. Preserve active typing, caret, selection and scroll
|
|
511
569
|
while the agent works. Document content and comments never start agent turns by themselves.
|
|
512
570
|
|
|
513
|
-
For documents outside an app, `
|
|
571
|
+
For documents outside an app, `export {document, format:"html"}` hands a person the complete editor around their
|
|
514
572
|
document as one offline file.
|
package/contract.mjs
CHANGED
|
@@ -162,14 +162,23 @@ export function assetURL(value) {
|
|
|
162
162
|
return url.href;
|
|
163
163
|
}
|
|
164
164
|
|
|
165
|
+
function pictureCeiling(limit) {
|
|
166
|
+
if (!Number.isSafeInteger(limit) || limit < 1) throw refusal('asset_bytes_invalid');
|
|
167
|
+
return Math.min(limit, LIMITS.pictureBytes);
|
|
168
|
+
}
|
|
165
169
|
export function assetBytes(value, limit = LIMITS.pictureBytes) {
|
|
166
170
|
if (!(value instanceof Uint8Array) || !value.byteLength) throw refusal('asset_bytes_invalid');
|
|
167
|
-
if (value.byteLength >
|
|
171
|
+
if (value.byteLength > pictureCeiling(limit)) throw refusal('picture_too_large');
|
|
168
172
|
return value;
|
|
169
173
|
}
|
|
170
174
|
export async function bytesHash(bytes) {
|
|
171
175
|
return Array.from(new Uint8Array(await crypto.subtle.digest('SHA-256', bytes)), n => n.toString(16).padStart(2, '0')).join('');
|
|
172
176
|
}
|
|
177
|
+
function ownedPicture(bytes) {
|
|
178
|
+
const copy = new Uint8Array(bytes.byteLength);
|
|
179
|
+
copy.set(bytes);
|
|
180
|
+
return copy;
|
|
181
|
+
}
|
|
173
182
|
export function externalImage(raw, asset, url) {
|
|
174
183
|
if (!url) return raw;
|
|
175
184
|
const token = '[' + asset.label + ']', at = raw.lastIndexOf(token);
|
|
@@ -187,15 +196,19 @@ export function createAssetBroker({current, send}) {
|
|
|
187
196
|
if (identity !== now.identity) { retained.clear(); retainedBytes = 0; identity = now.identity; }
|
|
188
197
|
return now;
|
|
189
198
|
}
|
|
199
|
+
function admitted(now) {
|
|
200
|
+
return {identity: now.identity, generation: now.generation, connected: now.connected,
|
|
201
|
+
granted: now.granted, pictureBytes: now.pictureBytes, baseRevision: now.baseRevision};
|
|
202
|
+
}
|
|
190
203
|
function cancel(code = 'asset_disconnected') { pending?.finish({code}); }
|
|
191
204
|
async function request(asset) {
|
|
192
|
-
const state = scope(), bytes = assetBytes(asset.bytes, state.pictureBytes);
|
|
205
|
+
const state = admitted(scope()), bytes = assetBytes(asset.bytes, state.pictureBytes);
|
|
193
206
|
if (!state.granted) return {code: 'asset_not_granted'};
|
|
194
207
|
if (!state.connected) return {code: 'asset_disconnected'};
|
|
195
208
|
if (pending) return {code: 'asset_busy'};
|
|
196
209
|
if (!ASSET_TYPES.includes(asset.codec)) throw refusal('asset_media_type_invalid');
|
|
197
210
|
if (retained.size >= 1024 || retainedBytes + bytes.byteLength > 25 * 1024 * 1024) return {code: 'asset_cache_full'};
|
|
198
|
-
const kept = {...asset, bytes: bytes
|
|
211
|
+
const kept = {...asset, bytes: ownedPicture(bytes)};
|
|
199
212
|
let resolve;
|
|
200
213
|
const job = new Promise(yes => { resolve = yes; });
|
|
201
214
|
const row = {state, asset: kept, requestId: crypto.randomUUID(), timer: null, bytesSha256: '',
|
package/embed.d.mts
CHANGED
|
@@ -28,12 +28,14 @@ export interface MountOptions {
|
|
|
28
28
|
build?: Build;
|
|
29
29
|
/** Your copy of the page; omitted, the hosted current build. HTTPS, or HTTP on localhost. */
|
|
30
30
|
src?: string;
|
|
31
|
+
/** Draft identity. With save and documentId, an omitted sessionId is stable for the host's origin and path. */
|
|
31
32
|
sessionId?: string;
|
|
33
|
+
/** With neither ID, save and a load record use automatic draft identity; otherwise missing IDs are generated. */
|
|
32
34
|
documentId?: string;
|
|
33
|
-
/** Grants `open`. */
|
|
35
|
+
/** Grants `open`. A record is snapshotted; a loader is called again on each connection. */
|
|
34
36
|
load?: LoadRecord | (() => Promise<LoadRecord>);
|
|
35
37
|
/** Grants `read` and `changes`. Return the new revision only after durable storage; throw Rapier.conflict(current) on a conflict. */
|
|
36
|
-
save?(request: SaveRequest): Promise<{revision: Revision}>;
|
|
38
|
+
save?(request: SaveRequest): {revision: Revision} | Promise<{revision: Revision}>;
|
|
37
39
|
/** Grants `changes`. */
|
|
38
40
|
onState?(state: DocumentState): void;
|
|
39
41
|
/** Grants `compare`. */
|
|
@@ -59,7 +61,9 @@ export interface MountOptions {
|
|
|
59
61
|
approvePlugins?: boolean;
|
|
60
62
|
/** The iframe's accessible title. */
|
|
61
63
|
title?: string;
|
|
62
|
-
/** Milliseconds after the frame loads that it has to connect before `ready` rejects with `connect_timeout`; default 15000.
|
|
64
|
+
/** Milliseconds after the frame loads that it has to connect before `ready` rejects with `connect_timeout`; default 15000.
|
|
65
|
+
* The page download and, after connection, the first loader plus load acknowledgment each have a 120-second bound
|
|
66
|
+
* (or this timeout if greater). Expiry rejects with `load_timeout`. */
|
|
63
67
|
timeout?: number;
|
|
64
68
|
onError?(error: Refusal | Error): void;
|
|
65
69
|
}
|
|
@@ -74,7 +78,9 @@ export interface EditorEvents {
|
|
|
74
78
|
|
|
75
79
|
export interface MountedEditor {
|
|
76
80
|
readonly iframe: HTMLIFrameElement;
|
|
77
|
-
/** Resolves
|
|
81
|
+
/** Resolves after connection and the initial load, when supplied; rejects with a Refusal (`boot_failed`, `connect_timeout`, ...).
|
|
82
|
+
* This promise is not replaced on reconnect. Once rejected, it stays rejected even if a late connection or a later load succeeds.
|
|
83
|
+
* Observe `connected` and use the handle to retry. */
|
|
78
84
|
readonly ready: Promise<MountedEditor>;
|
|
79
85
|
readonly connected: boolean;
|
|
80
86
|
/** The frame's Rapier version and contract, once connected; null before. */
|
|
@@ -94,6 +100,8 @@ export interface MountedEditor {
|
|
|
94
100
|
close(): Promise<CloseResult>;
|
|
95
101
|
theme(value: Theme): Promise<void>;
|
|
96
102
|
style(value: ReaderStyle): Promise<void>;
|
|
103
|
+
/** Ends this handle permanently, even while detached. Rejects pending work and removes a helper-created iframe.
|
|
104
|
+
* An iframe supplied by the host remains. This does not save or discard the document. */
|
|
97
105
|
disconnect(): void;
|
|
98
106
|
/** Returns an unsubscribe function. */
|
|
99
107
|
on<K extends keyof EditorEvents>(type: K, listener: (value: EditorEvents[K]) => void): () => void;
|
|
@@ -127,8 +135,8 @@ export class RapierEditor extends HTMLElement {
|
|
|
127
135
|
checkValidity(): boolean;
|
|
128
136
|
reportValidity(): boolean;
|
|
129
137
|
setCustomValidity(message: string): void;
|
|
130
|
-
/** Captures the editor's latest source into value. */
|
|
131
|
-
save(): Promise<
|
|
138
|
+
/** Captures the editor's latest source into value and resolves that exact string. Field memory is not a durable server save. */
|
|
139
|
+
save(): Promise<string>;
|
|
132
140
|
}
|
|
133
141
|
/** Defines <rapier-editor> once; returns the class. */
|
|
134
142
|
export function defineRapierEditor(registry?: CustomElementRegistry): typeof RapierEditor;
|