rapier-embed 1.2.33 → 1.2.40
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 +96 -28
- package/cli.mjs +19 -6
- package/contract.d.mts +3 -0
- package/contract.mjs +16 -0
- package/embed.d.mts +34 -7
- package/embed.mjs +132 -24
- package/package.json +1 -1
- package/rapier-plugins.json +188 -188
package/README.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Embed Rapier
|
|
2
2
|
|
|
3
|
+
Put Rapier's Markdown editor or reader in your web app, each one offline HTML file.
|
|
4
|
+
|
|
5
|
+
<picture>
|
|
6
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/jackskip22/rapier-plugins/main/assets/write-1280-dark.png">
|
|
7
|
+
<img src="https://raw.githubusercontent.com/jackskip22/rapier-plugins/main/assets/write-1280-light.png" alt="The Rapier editor on a computer: a launch plan with an agent's change marked" width="960">
|
|
8
|
+
</picture>
|
|
9
|
+
|
|
10
|
+
Try the editor at [rapier.website](https://rapier.website).
|
|
11
|
+
Install: `npm install rapier-embed`.
|
|
12
|
+
|
|
3
13
|
## Start here
|
|
4
14
|
|
|
5
15
|
For embed-only work, start with the supplied application code and the published embed contract below. Use the declared helper release or packaged helper, keeping source, permissions and storage under the host application. Do not create a hosted document. Keep the embed's own WebMCP contract; do not substitute connector calls for its browser API.
|
|
@@ -7,10 +17,10 @@ For embed-only work, start with the supplied application code and the published
|
|
|
7
17
|
For an explanation-only request, answer in chat without creating a document. If required content or integration details are missing, ask for them; do not create an empty hosted workspace.
|
|
8
18
|
Honor no-upload or no-host instructions. The workflow choices below are alternatives, not steps to run. Skip hosted suggestions, including opening a workspace or creating a return link, for local-only or embed-only work.
|
|
9
19
|
|
|
10
|
-
Embed Rapier's Markdown editor or lightweight reader in your web app. Each build is one offline HTML file. Your
|
|
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
|
|
11
21
|
app owns the document and storage; its agent edits live through WebMCP. The person sees the agent's presence and
|
|
12
22
|
change marks, taps a change to see the before and undoes anything. Honor the person's Will/1 regions.
|
|
13
|
-
The reader is one file of about
|
|
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
|
|
14
24
|
buy a commercial license for other terms (`LICENSING.md` in the Rapier repository).
|
|
15
25
|
|
|
16
26
|
Choose one of two builds:
|
|
@@ -22,8 +32,10 @@ Choose one of two builds:
|
|
|
22
32
|
|
|
23
33
|
Both render the same Markdown standard with the same layout. Download the reader from the root of the
|
|
24
34
|
[rapier repository](https://github.com/jackskip22/rapier), or frame the hosted copies:
|
|
25
|
-
`https://rapier.website/embed/rapier-reader.html` and `https://rapier.website/embed/rapier.html` (current).
|
|
26
|
-
Each release's own files stay at its tag: `https://cdn.jsdelivr.net/gh/jackskip22/rapier@v<version>/site/embed/<file
|
|
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
|
|
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
|
+
downloaded copy yourself and pass it as `src`.
|
|
27
39
|
|
|
28
40
|
`rapier-embed` mounts the reader or the editor in an iframe and speaks the
|
|
29
41
|
[embed contract](https://github.com/jackskip22/rapier/blob/main/docs/embed-contract.md) for you.
|
|
@@ -38,7 +50,7 @@ Each release's own files stay at its tag: `https://cdn.jsdelivr.net/gh/jackskip2
|
|
|
38
50
|
document access remain enforced.
|
|
39
51
|
|
|
40
52
|
```sh
|
|
41
|
-
npm install rapier-embed@1.2.
|
|
53
|
+
npm install rapier-embed@1.2.40
|
|
42
54
|
```
|
|
43
55
|
|
|
44
56
|
Or copy the package's `embed.mjs` and `contract.mjs` into one directory of your app; no runtime CDN is needed.
|
|
@@ -52,11 +64,11 @@ sends Markdown only when saving; change notifications contain no source.
|
|
|
52
64
|
Rapier is a phone-first Markdown editor for notes, diagrams, drawing and watercolor painting. It runs offline
|
|
53
65
|
as one HTML file on Android, Web, Windows and Linux.
|
|
54
66
|
Collaborate over MCP and WebMCP, use optional plain-file sync to your own cloud storage, or embed the
|
|
55
|
-
editor and lightweight reader (about
|
|
67
|
+
editor and lightweight reader (about 370 kB). The agent edits live; the person sees its presence and
|
|
56
68
|
changes, taps a change to see the before and undoes anything. Use `show {target:"comparison"}` to show a diff deliberately.
|
|
57
69
|
|
|
58
70
|
1. **Work together live.** Connect to `https://mcp.rapier.website/door/mcp`, then call `open` to share one document with the person.
|
|
59
|
-
2. **Deliver an offline editor.** Run `npx rapier-html@1.2.
|
|
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)).
|
|
60
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.
|
|
61
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.
|
|
62
74
|
5. **Encode JPEG XL.** Install [rapier-jxl](https://github.com/jackskip22/rapier-jxl) to encode pixels, photographs and existing JPEGs in JavaScript.
|
|
@@ -92,6 +104,23 @@ Its `settings` features are `find`, `readAloud` and `share`; pass `settings: {fe
|
|
|
92
104
|
others out. For a plain iframe, put the options in its address:
|
|
93
105
|
`<iframe src="/vendor/rapier/rapier-reader.html?plugins=/vendor/rapier/plugins/">`.
|
|
94
106
|
|
|
107
|
+
## Where it can be framed
|
|
108
|
+
|
|
109
|
+
The frame needs the host page to be an `https` page, or `http` on `localhost`. `Rapier.mount` checks this before it requests anything, and `ready` rejects at once with a code:
|
|
110
|
+
|
|
111
|
+
| Host page | `ready` rejects with | Why and what to do |
|
|
112
|
+
| --- | --- | --- |
|
|
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
|
+
| `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
|
+
| 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
|
+
| 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
|
+
|
|
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."
|
|
119
|
+
|
|
120
|
+
### Open a document by link
|
|
121
|
+
|
|
122
|
+
The reader on its own page opens a document at an address on its own origin: `https://your.site/vendor/rapier/rapier-reader.html#src=/plans/a.md`. Word and PDF files open the same way. An address on another origin is refused in words, because the page's security policy reads only its own origin. A framed reader takes its document from `load` and ignores the link. The hosted page, `https://rapier.website/embed/rapier-reader.html`, has no documents of yours on its origin: for it, use `Rapier.mount`.
|
|
123
|
+
|
|
95
124
|
## Keep Rapier in your codebase
|
|
96
125
|
|
|
97
126
|
A copy in your app works offline and changes only when you update it.
|
|
@@ -99,7 +128,7 @@ A copy in your app works offline and changes only when you update it.
|
|
|
99
128
|
1. Download the build you frame into a static directory, for example
|
|
100
129
|
`https://cdn.jsdelivr.net/gh/jackskip22/rapier@v<version>/site/embed/rapier-reader.html` to `public/vendor/rapier/`.
|
|
101
130
|
2. Fetch the plug-ins your build uses into one directory beside it, from the manifest of the same version:
|
|
102
|
-
`npx rapier-embed@1.2.
|
|
131
|
+
`npx rapier-embed@1.2.40 plugins public/vendor/rapier/plugins --build reader`.
|
|
103
132
|
3. Mount with `src` and `plugins` pointing at your copies. Serve them over HTTPS (HTTP on localhost) and allow
|
|
104
133
|
their origin in your `frame-src`. A same-origin parent can script its frame; use a dedicated origin when the
|
|
105
134
|
frame must be isolated.
|
|
@@ -111,18 +140,28 @@ To update, replace the page and its plug-ins with those of the new version toget
|
|
|
111
140
|
Large optional renderers are plug-ins: pinned files a page fetches the first time a document needs one, checks by
|
|
112
141
|
length and SHA-384 before it runs it, and keeps on the device. A file that fails its check is refused.
|
|
113
142
|
|
|
114
|
-
|
|
143
|
+
<!-- PLUGIN_TABLE_BEGIN (tools/embed-plugin-table.mjs writes this table) -->
|
|
144
|
+
| Plug-in | Adds | Size | Pages |
|
|
115
145
|
| --- | --- | --- | --- |
|
|
116
|
-
| `flowchart` | Mermaid flowcharts and graphs,
|
|
117
|
-
| `
|
|
118
|
-
| `
|
|
119
|
-
| `
|
|
120
|
-
| `
|
|
121
|
-
| `ocr` |
|
|
122
|
-
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
`
|
|
146
|
+
| `flowchart` | Draws Mermaid flowcharts and graphs natively, in the editor's own style. | 0.32 MB | The reader |
|
|
147
|
+
| `docx` | Opens a Word document (.docx) as a Markdown document: headings, lists, tables, pictures and comments. | 0.61 MB | The reader |
|
|
148
|
+
| `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 |
|
|
150
|
+
| `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`) |
|
|
152
|
+
| `font-subset` | Keeps the used characters in fonts embedded in exported pages. | 1.21 MB | `rapier.html` |
|
|
153
|
+
| `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
|
+
| `templates` | Starters' traced page borders: 72 borders from the ornament book. | 6.03 MB (72 files) | `rapier.html` |
|
|
155
|
+
| `elements` | The Elements sheet's traced ornaments: 3,155 pieces from the ornament book. | 16.2 MB (51 files) | `rapier.html` |
|
|
156
|
+
| `lettersets` | The Gallery's letter sets: 31 sets of ornamental capitals. | 2.82 MB (31 files) | `rapier.html` |
|
|
157
|
+
|
|
158
|
+
Totals: the reader, 10.4 MB (31.9 MB with `ocr`); `rapier.html`, 58.5 MB. Release 1.2.40.
|
|
159
|
+
<!-- PLUGIN_TABLE_END -->
|
|
160
|
+
|
|
161
|
+
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`.
|
|
163
|
+
|
|
164
|
+
**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.
|
|
126
165
|
|
|
127
166
|
**With `plugins`.** With `plugins` set, a page (the reader or `rapier.html`) reads every plug-in from
|
|
128
167
|
that directory, by each file's path in the manifest, and makes no other request, so it works offline and under a strict
|
|
@@ -133,7 +172,7 @@ The directory must be on the page's own origin, because the page's Content Secur
|
|
|
133
172
|
throws a `TypeError` for a directory on another origin, and a page opened with one shows each plug-in as failed, with the
|
|
134
173
|
reason, without making a request. To keep the plug-ins on a CDN, serve the page from the same origin.
|
|
135
174
|
|
|
136
|
-
`npx rapier-embed@1.2.
|
|
175
|
+
`npx rapier-embed@1.2.40 plugins <directory>` fetches every file the manifest lists, checks it, and writes it. It
|
|
137
176
|
keeps a file that is already there and correct, and exits with status 1 if any file is missing or fails its check.
|
|
138
177
|
`--build reader` or `--build full` keeps the files that page uses, `--only name,name` fetches some
|
|
139
178
|
plug-ins, `--check` verifies a directory without fetching, `--from <address>` fetches from your own mirror, and
|
|
@@ -160,7 +199,18 @@ done
|
|
|
160
199
|
|
|
161
200
|
**Without `plugins`.** `rapier.html` looks for the `math` and `diagrams` files beside the page,
|
|
162
201
|
at the file names the manifest lists, before their pinned addresses. The
|
|
163
|
-
PDF reader, text in pictures and letter sets come from their pinned addresses.
|
|
202
|
+
PDF reader, text in pictures and letter sets come from their pinned addresses. The reader reads every plug-in from its
|
|
203
|
+
pinned address and never looks beside itself.
|
|
204
|
+
|
|
205
|
+
## Relative pictures
|
|
206
|
+
|
|
207
|
+
A relative picture address in a document (``) is read from the address you give in `baseUrl`, an absolute https address (http on `localhost`, `127.0.0.1` or `[::1]` too, so you can try it locally), in `load` or `handle.load()`. It is an option of `load`, not of `mount`:
|
|
208
|
+
|
|
209
|
+
```js
|
|
210
|
+
await reader.load(planText, {revision: 2, baseUrl: 'https://example.com/docs/plan/'}); // images/a.png -> https://example.com/docs/plan/images/a.png
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
The Markdown keeps the address as written; only the displayed address changes, and a save writes it back unchanged. Without `baseUrl`, a relative picture resolves against Rapier's own origin and does not load. Your page's security policy and the picture host's CORS rules still apply. A `baseUrl` that is neither is refused with `invalid_base_url`.
|
|
164
214
|
|
|
165
215
|
## Style the reader
|
|
166
216
|
|
|
@@ -235,12 +285,11 @@ const editor = Rapier.mount(document.querySelector('#editor'), {
|
|
|
235
285
|
await editor.ready;
|
|
236
286
|
```
|
|
237
287
|
|
|
238
|
-
Mount takes a container or an iframe and owns its navigation, title, sandbox
|
|
239
|
-
allow-downloads`) and clipboard permission. It adds `?embed=1`, keeps any other query, binds its listener before
|
|
288
|
+
Mount takes a container or an iframe and owns its navigation, title and clipboard permission. On an iframe it creates it sets the sandbox `allow-scripts allow-same-origin allow-downloads`. On an iframe you pass, a `sandbox` you wrote stays as written, and `ready` rejects with `sandbox_token_missing` if it lacks `allow-scripts` or `allow-same-origin`. It adds `?embed=1`, keeps any other query, binds its listener before
|
|
240
289
|
navigation, and accepts readiness only from that iframe's window and exact origin. HTTPS is required, except HTTP
|
|
241
290
|
on localhost.
|
|
242
291
|
|
|
243
|
-
`load` is a record or an async callback returning `{content, revision, filename?, readOnly?, title?}`. The `save`
|
|
292
|
+
`load` is a record or an async callback returning `{content, revision, filename?, readOnly?, title?, baseUrl?}`. The `save`
|
|
244
293
|
callback receives `{content, filename, docKind, codeLang, requestId, baseRevision}` and returns `{revision}` only
|
|
245
294
|
after durable storage. A repeated request ID shares the first write and repeats its first answer, even while that
|
|
246
295
|
write is pending. A repeated ID with different source, metadata or base revision is refused as
|
|
@@ -249,6 +298,10 @@ Oversized source is `document_too_large`; ill-formed Unicode and malformed metad
|
|
|
249
298
|
The helper holds answers for its lifetime; your store must also deduplicate IDs across host
|
|
250
299
|
reloads. Keep stable session and document IDs when you intend to recover a session; omitted IDs are generated.
|
|
251
300
|
|
|
301
|
+
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.
|
|
302
|
+
|
|
303
|
+
Reading the text: the editor sends source only to your `save` callback. `save()` with no edits since the last load or save resolves the current revision and does not call your callback. There is no read that skips `save`; keep the last saved text in memory if you need it.
|
|
304
|
+
|
|
252
305
|
A save delayed beyond fifteen seconds remains pending in the editor; it can still be confirmed. `editor.save()`
|
|
253
306
|
waits for the editor to accept the durable revision with its latest edits saved. An unacknowledged write never
|
|
254
307
|
becomes a successful save because time passed. On conflict, throw `Rapier.conflict(currentRevision)`; the person's
|
|
@@ -323,27 +376,42 @@ apply the old insertion to the replacement document. Asset failures do not rejec
|
|
|
323
376
|
| `assets(request)` | Grants `assets` only; the callback stores picture bytes and returns `{url}` |
|
|
324
377
|
| `settings`, `theme`, `style` | Session settings, `'light'`, `'dark'` or `'system'`, and the reader's look |
|
|
325
378
|
| `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
|
+
| `approvePlugins` | `true`: install math and diagram plug-ins without asking the person. Naming `plugins` does the same. Default `false` |
|
|
326
380
|
| `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 |
|
|
327
382
|
|
|
328
383
|
The handle has `iframe`, `ready` (a promise) and `connected`. `load(content, {revision, filename?, readOnly?,
|
|
329
384
|
title?})`, `save()`, `compare(content, {filename?})` and `close()` use their grants. `theme(value)` changes the
|
|
330
385
|
host theme; `style({css, fonts})` restyles the reader; `disconnect()` ends the connection, rejects unfinished
|
|
331
386
|
operations and removes the iframe that mount created in a container (an iframe you passed stays). Mount does not size
|
|
332
387
|
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.
|
|
333
390
|
`on('connected' | 'state' | 'close-request' | 'closed' | 'error', fn)` returns an
|
|
334
391
|
unsubscribe function. State is `{loaded, dirty, saving, closing, readOnly, filename, docKind}`. Mount a fresh frame to
|
|
335
392
|
change grants or settings. A denied or dirty replacement load rejects.
|
|
336
393
|
|
|
337
394
|
Every refusal is an `Error` with a `code`. A connect the frame refuses rejects `ready`: `contract_unsupported`,
|
|
338
|
-
`capabilities_invalid`, a `settings_*` code, or `invalid_payload` with a `reason`.
|
|
339
|
-
`
|
|
395
|
+
`capabilities_invalid`, a `settings_*` code, or `invalid_payload` with a `reason`. A frame that cannot start rejects
|
|
396
|
+
`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
|
|
400
|
+
`TypeError` with a code for an unknown option (`option_unknown`), a build whose `src` names the other page (`build_mismatch`)
|
|
401
|
+
and a reader feature the reader does not have (`settings_feature_unavailable`). `Rapier.version` is this package's version;
|
|
402
|
+
`handle.version` and `handle.contract` are the frame's, once connected. `load()` resolves `{revision, recoveredDraft,
|
|
403
|
+
readOnly}`; `compare()` resolves `{filename}`. `save()` resolves with
|
|
404
|
+
`{revision}` once the frame holds no newer edits (with no edits since the last load or save, the current revision, and
|
|
405
|
+
your callback is not called), and rejects with `conflict`, `failed` (your callback threw),
|
|
340
406
|
`document_too_large`, `save_payload_invalid`, `not_loaded` (no load yet), `read_only` or `save_unavailable` (the
|
|
341
407
|
frame could not capture; the person is told why). `close()` resolves with the frame's `close-ready` payload:
|
|
342
408
|
`{saved: true}` or `{discarded: true}` after your answer, `{forced: true}` when the person closes without your
|
|
343
409
|
answer, or `{cancelled: true}` when you answer `cancel`.
|
|
344
410
|
|
|
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.
|
|
412
|
+
|
|
345
413
|
The frame keeps the person's unsaved work only for the document you loaded: a draft in that tab's session storage,
|
|
346
|
-
which a load of the same revision reopens and your `discard` answer retires. It never reads, offers or writes the
|
|
414
|
+
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
|
|
347
415
|
person's own Rapier documents or recovery on its origin, and it keeps nothing before your first load.
|
|
348
416
|
|
|
349
417
|
Saving names the browser-authenticated host and port. The helper's acknowledgment is your statement that storage
|
|
@@ -426,5 +494,5 @@ Will/1 records the person's instructions in their document. Respect `keep`, appe
|
|
|
426
494
|
Documents without Will regions are editable throughout. Preserve active typing, caret, selection and scroll
|
|
427
495
|
while the agent works. Document content and comments never start agent turns by themselves.
|
|
428
496
|
|
|
429
|
-
For documents outside an app, `npx rapier-html@1.2.
|
|
497
|
+
For documents outside an app, `npx rapier-html@1.2.40 notes.md` hands a person the complete editor around their
|
|
430
498
|
document as one offline file.
|
package/cli.mjs
CHANGED
|
@@ -9,7 +9,8 @@ import {fileURLToPath} from 'node:url';
|
|
|
9
9
|
const BUILDS = ['reader', 'full'], buildName = build => build === 'document' ? 'full' : build;
|
|
10
10
|
const MANIFEST_BYTES = 1024 * 1024, FILE_BYTES = 64 * 1024 * 1024, TOTAL_BYTES = 512 * 1024 * 1024;
|
|
11
11
|
const NOFOLLOW = constants.O_NOFOLLOW || 0;
|
|
12
|
-
const usage = 'usage: rapier-embed plugins <directory> [--build reader|full] [--only name,name] [--from <address>] [--manifest <file or address>] [--check]';
|
|
12
|
+
const usage = 'usage: rapier-embed plugins <directory> [--build reader|full] [--with ocr] [--only name,name] [--from <address>] [--manifest <file or address>] [--check]';
|
|
13
|
+
const OPTIONAL = {reader: ['ocr']};
|
|
13
14
|
|
|
14
15
|
function arguments_(argv) {
|
|
15
16
|
const [command, directory, ...rest] = argv, options = {};
|
|
@@ -17,7 +18,7 @@ function arguments_(argv) {
|
|
|
17
18
|
for (let at = 0; at < rest.length; at++) {
|
|
18
19
|
const name = rest[at];
|
|
19
20
|
if (name === '--check') options.check = true;
|
|
20
|
-
else if (['--build', '--only', '--from', '--manifest'].includes(name) && rest[at + 1] !== undefined) options[name.slice(2)] = rest[++at];
|
|
21
|
+
else if (['--build', '--with', '--only', '--from', '--manifest'].includes(name) && rest[at + 1] !== undefined) options[name.slice(2)] = rest[++at];
|
|
21
22
|
else throw new Error(usage);
|
|
22
23
|
}
|
|
23
24
|
if (options.build !== undefined) options.build = buildName(options.build);
|
|
@@ -60,8 +61,15 @@ async function fetchChecked(address, limit) {
|
|
|
60
61
|
const url = new URL(address);
|
|
61
62
|
if (url.username || url.password || !(url.protocol === 'https:' || url.protocol === 'http:' && ['localhost', '127.0.0.1', '[::1]'].includes(url.hostname)))
|
|
62
63
|
throw new Error('a plug-in address needs HTTPS, or HTTP on localhost, without credentials');
|
|
63
|
-
const
|
|
64
|
-
|
|
64
|
+
const ask = () => fetch(url, {credentials: 'omit', referrerPolicy: 'no-referrer', redirect: 'error', signal: AbortSignal.timeout(60000)});
|
|
65
|
+
let response = await ask();
|
|
66
|
+
if (response.status === 503 || response.status === 504) {
|
|
67
|
+
await response.body?.cancel();
|
|
68
|
+
await new Promise(done => setTimeout(done, 2000));
|
|
69
|
+
response = await ask();
|
|
70
|
+
if (!response.ok) { await response.body?.cancel(); throw new Error('HTTP ' + response.status + ' from ' + url.href + ' twice; try again later, or fetch from another address with --from'); }
|
|
71
|
+
}
|
|
72
|
+
if (!response.ok) { await response.body?.cancel(); throw new Error('HTTP ' + response.status + ' from ' + url.href); }
|
|
65
73
|
const length = response.headers.get('content-length');
|
|
66
74
|
if (length !== null && (!/^\d+$/.test(length) || !Number.isSafeInteger(Number(length)) || Number(length) > limit)) {
|
|
67
75
|
await response.body?.cancel(); throw new Error('response exceeds the permitted ' + limit + ' bytes');
|
|
@@ -102,11 +110,16 @@ export async function plugins(argv) {
|
|
|
102
110
|
let failed = 0;
|
|
103
111
|
await mkdir(options.directory, {recursive: true});
|
|
104
112
|
const directory = await realpath(options.directory);
|
|
105
|
-
const
|
|
113
|
+
const wanted = new Set(options.with ? options.with.split(',') : []);
|
|
114
|
+
for (const name of wanted) if (!names.has(name)) throw new Error('no plug-in named ' + name + '; the manifest has ' + [...names].join(', '));
|
|
115
|
+
const optional = plugin => !!options.build && (OPTIONAL[options.build] || []).includes(plugin.name) && !wanted.has(plugin.name) && !only?.has(plugin.name);
|
|
116
|
+
const uses = (plugin, file) => !options.build || !optional(plugin) && (file.builds || plugin.builds || BUILDS).map(buildName).includes(options.build);
|
|
106
117
|
const jobs = manifest.plugins.filter(plugin => !only || only.has(plugin.name)).flatMap(plugin => plugin.files.filter(file => uses(plugin, file)).map(file => ({plugin, file})));
|
|
107
118
|
if (!jobs.length) throw new Error('nothing in the manifest matches that --build and --only');
|
|
108
119
|
const total = jobs.reduce((sum, {file}) => sum + file.bytes, 0);
|
|
109
|
-
|
|
120
|
+
const skipped = manifest.plugins.filter(optional).map(plugin => plugin.name);
|
|
121
|
+
console.log(jobs.length + ' files, ' + (total / 1048576).toFixed(1) + ' MiB' + (options.build ? ' for the ' + options.build + ' build' : ' for every build; --build reader or full keeps only what that page uses') +
|
|
122
|
+
(skipped.length ? '; without ' + skipped.join(', ') + ' (add --with ' + skipped.join(',') + ')' : ''));
|
|
110
123
|
const work = async ({plugin, file}) => {
|
|
111
124
|
const name = plugin.name + ' ' + file.file;
|
|
112
125
|
let temp = null;
|
package/contract.d.mts
CHANGED
|
@@ -43,3 +43,6 @@ export function bytesHash(bytes: BufferSource): Promise<string>;
|
|
|
43
43
|
export function externalImage(raw: string, asset: {label: string; title?: string}, url: string | null | undefined): string;
|
|
44
44
|
/** The editor page's picture exchange; used by the page itself. */
|
|
45
45
|
export function createAssetBroker(ports: Record<string, unknown>): unknown;
|
|
46
|
+
/** Not a mount option: the option is `baseUrl` on `load`. The host's picture base: returns a function that turns a relative picture address into an address under `baseUrl` (or null), with
|
|
47
|
+
* `vouched`, the addresses it produced. Null when no base is given; throws `invalid_base_url` for anything but an absolute https address (or http on localhost). Used by the page itself. */
|
|
48
|
+
export function pictureBase(value: string | null | undefined): (((src: string) => string | null) & {vouched: Set<string>}) | null;
|
package/contract.mjs
CHANGED
|
@@ -237,3 +237,19 @@ export function createAssetBroker({current, send}) {
|
|
|
237
237
|
function lookup(url) { scope(); return retained.get(url)?.asset || null; }
|
|
238
238
|
return Object.freeze({request, answer, cancel, lookup});
|
|
239
239
|
}
|
|
240
|
+
|
|
241
|
+
export function pictureBase(value) {
|
|
242
|
+
if (value === undefined || value === null) return null;
|
|
243
|
+
let base;
|
|
244
|
+
try {
|
|
245
|
+
if (typeof value !== 'string' || !value || value.length > 4096 || /[\s\u0000-\u001f\u007f<>\\]/u.test(value)) throw 0;
|
|
246
|
+
base = new URL(value);
|
|
247
|
+
if (base.username || base.password || !(base.protocol === 'https:' || base.protocol === 'http:' && ['localhost', '127.0.0.1', '[::1]'].includes(base.hostname))) throw 0;
|
|
248
|
+
} catch (_) { throw refusal('invalid_base_url', 'baseUrl is an absolute https address, or http on localhost.'); }
|
|
249
|
+
const vouched = new Set();
|
|
250
|
+
return Object.assign(src => {
|
|
251
|
+
const text = String(src || '').trim();
|
|
252
|
+
if (!text || /^(?:[a-z][a-z0-9+.-]*:|\/\/|#)/i.test(text)) return null;
|
|
253
|
+
try { const href = new URL(text, base).href; vouched.add(href); return href; } catch (_) { return null; }
|
|
254
|
+
}, {vouched});
|
|
255
|
+
}
|
package/embed.d.mts
CHANGED
|
@@ -10,11 +10,17 @@ export type DocKind = 'markdown' | 'text' | 'code';
|
|
|
10
10
|
/** `full` is rapier.html (Draw, Paint, Notes, agents); `reader` is the read-only reader. `document` names `full`. */
|
|
11
11
|
export type Build = 'full' | 'reader' | 'document';
|
|
12
12
|
|
|
13
|
-
export interface LoadRecord { content: string; revision: Revision; filename?: string; readOnly?: boolean; title?: string;
|
|
13
|
+
export interface LoadRecord { content: string; revision: Revision; filename?: string; readOnly?: boolean; title?: string;
|
|
14
|
+
/** An absolute https address (http on localhost) that relative pictures in this document are read from. The Markdown keeps what it says. */
|
|
15
|
+
baseUrl?: string; }
|
|
14
16
|
export interface SaveRequest { content: string; filename: string; docKind: DocKind; codeLang: string; requestId: string; baseRevision: Revision; }
|
|
15
17
|
export interface DocumentState { loaded: boolean; dirty: boolean; saving: boolean; closing: boolean; readOnly: boolean; filename?: string; docKind?: DocKind; }
|
|
16
18
|
export interface AssetRequest { requestId: string; baseRevision: Revision; bytesSha256: string; mediaType: AssetType; bytes: Uint8Array; signal: AbortSignal; }
|
|
17
19
|
export interface ReaderStyle { css?: string; fonts?: {family: string; source: string | ArrayBuffer | ArrayBufferView; descriptors?: FontFaceDescriptors}[]; }
|
|
20
|
+
/** What load() resolves: the revision the frame now holds, whether it brought back the person's unsaved draft, and read-only. */
|
|
21
|
+
export interface LoadResult { revision: Revision; recoveredDraft: boolean; readOnly: boolean; }
|
|
22
|
+
/** What compare() resolves: the name the comparison shows. */
|
|
23
|
+
export interface CompareResult { filename: string; }
|
|
18
24
|
export type CloseAnswer = 'save' | 'discard' | 'cancel';
|
|
19
25
|
export type CloseResult = {saved: true} | {discarded: true} | {forced: true} | {cancelled: true};
|
|
20
26
|
|
|
@@ -40,7 +46,8 @@ export interface MountOptions {
|
|
|
40
46
|
agentName?: string;
|
|
41
47
|
/** Grants `assets`: store the picture bytes and return their absolute HTTPS address. */
|
|
42
48
|
assets?(request: AssetRequest): Promise<{url: string}>;
|
|
43
|
-
/** Chooses the file the person opens
|
|
49
|
+
/** Chooses the file the person opens when they tap Open in the frame (the host's own picker, for example in a WebView).
|
|
50
|
+
* Called with the frame's request; resolve a File, or null when the person cancels. Omitted, the browser's file picker. */
|
|
44
51
|
openFile?(request: {requestId: string; baseRevision: Revision; signal: AbortSignal}): File | null | Promise<File | null>;
|
|
45
52
|
settings?: Settings;
|
|
46
53
|
theme?: Theme;
|
|
@@ -48,8 +55,12 @@ export interface MountOptions {
|
|
|
48
55
|
style?: ReaderStyle;
|
|
49
56
|
/** The directory that holds the plug-in files, on the page's own origin. */
|
|
50
57
|
plugins?: string;
|
|
58
|
+
/** true: the frame installs math and diagram plug-ins without asking the person. Naming `plugins` has the same effect. Default false. */
|
|
59
|
+
approvePlugins?: boolean;
|
|
51
60
|
/** The iframe's accessible title. */
|
|
52
61
|
title?: string;
|
|
62
|
+
/** Milliseconds a loaded frame has to connect before `ready` rejects with `connect_timeout`; default 15000. */
|
|
63
|
+
timeout?: number;
|
|
53
64
|
onError?(error: Refusal | Error): void;
|
|
54
65
|
}
|
|
55
66
|
|
|
@@ -63,13 +74,23 @@ export interface EditorEvents {
|
|
|
63
74
|
|
|
64
75
|
export interface MountedEditor {
|
|
65
76
|
readonly iframe: HTMLIFrameElement;
|
|
66
|
-
/** Resolves once the frame accepts the grants; rejects with a Refusal. */
|
|
67
|
-
readonly ready: Promise<
|
|
77
|
+
/** Resolves with this handle once the frame accepts the grants; rejects with a Refusal (`boot_failed`, `connect_timeout`, ...). */
|
|
78
|
+
readonly ready: Promise<MountedEditor>;
|
|
68
79
|
readonly connected: boolean;
|
|
69
|
-
|
|
80
|
+
/** The frame's Rapier version and contract, once connected; null before. */
|
|
81
|
+
readonly version: string | null;
|
|
82
|
+
readonly contract: number | null;
|
|
83
|
+
/** Shows another document. `revision` is optional here: omitted, the helper uses one above the last revision it saw, or 1.
|
|
84
|
+
* Rejects with `revision_stale` for a revision older than the frame's and `revision_conflict` for the same revision with
|
|
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
|
|
87
|
+
* `document_unshowable` when the reader cannot show the document. */
|
|
88
|
+
load(content: string, options?: {revision?: Revision; filename?: string; readOnly?: boolean; title?: string; baseUrl?: string}): Promise<LoadResult>;
|
|
89
|
+
/** The same, with the record `mount` takes: `load({content, revision, filename})`. */
|
|
90
|
+
load(record: {content: string; revision?: Revision; filename?: string; readOnly?: boolean; title?: string; baseUrl?: string}): Promise<LoadResult>;
|
|
70
91
|
/** Resolves once the frame holds no newer edits than the durable revision. */
|
|
71
92
|
save(): Promise<{revision: Revision}>;
|
|
72
|
-
compare(content: string, options?: {filename?: string}): Promise<
|
|
93
|
+
compare(content: string, options?: {filename?: string}): Promise<CompareResult>;
|
|
73
94
|
close(): Promise<CloseResult>;
|
|
74
95
|
theme(value: Theme): Promise<void>;
|
|
75
96
|
style(value: ReaderStyle): Promise<void>;
|
|
@@ -79,10 +100,16 @@ export interface MountedEditor {
|
|
|
79
100
|
}
|
|
80
101
|
|
|
81
102
|
export const Rapier: {
|
|
82
|
-
/** Frames Rapier in an iframe or a container. Throws TypeError for an invalid option
|
|
103
|
+
/** Frames Rapier in an iframe or a container. Throws a TypeError for an invalid option, with a code for an unknown option
|
|
104
|
+
* (`option_unknown`), a build that does not match its page (`build_mismatch`) and a reader feature the reader lacks
|
|
105
|
+
* (`settings_feature_unavailable`). */
|
|
83
106
|
mount(target: HTMLIFrameElement | HTMLElement, options?: MountOptions): MountedEditor;
|
|
84
107
|
/** The error a save callback throws when the stored document has changed. */
|
|
85
108
|
conflict(currentRevision: Revision): Error & {code: 'conflict'; currentRevision: Revision};
|
|
109
|
+
/** This package's version. */
|
|
110
|
+
readonly version: string;
|
|
111
|
+
/** The host contract this package speaks. */
|
|
112
|
+
readonly contract: 1;
|
|
86
113
|
};
|
|
87
114
|
|
|
88
115
|
/** A form-associated field that submits its exact Markdown as a text/markdown file part. */
|