rapier-embed 1.2.8 → 1.2.10
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 +7 -7
- package/cli.mjs +0 -10
- package/contract.mjs +0 -7
- package/embed.mjs +0 -30
- package/package.json +1 -1
- package/rapier-plugins.json +894 -29
package/README.md
CHANGED
|
@@ -10,8 +10,8 @@ Honor no-upload or no-host instructions. The workflow choices below are alternat
|
|
|
10
10
|
Embed Rapier's Markdown editor or lightweight reader in your web app. Each build is one offline HTML file. Your
|
|
11
11
|
app owns the document and storage; its agent edits live through WebMCP. The person sees the agent's presence and
|
|
12
12
|
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 360 kB, with optional fonts and plug-ins. The helper is MIT with no dependencies
|
|
14
|
-
|
|
13
|
+
The reader is one file of about 360 kB, 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
|
+
buy a commercial license for other terms (`LICENSING.md` in the Rapier repository).
|
|
15
15
|
|
|
16
16
|
Choose one of two builds:
|
|
17
17
|
|
|
@@ -38,7 +38,7 @@ Each release's own files stay at its tag: `https://cdn.jsdelivr.net/gh/jackskip2
|
|
|
38
38
|
document access remain enforced.
|
|
39
39
|
|
|
40
40
|
```sh
|
|
41
|
-
npm install rapier-embed@1.2.
|
|
41
|
+
npm install rapier-embed@1.2.10
|
|
42
42
|
```
|
|
43
43
|
|
|
44
44
|
Or copy the package's `embed.mjs` and `contract.mjs` into one directory of your app; no runtime CDN is needed.
|
|
@@ -56,7 +56,7 @@ editor and lightweight reader (about 360 kB). The agent edits live; the person s
|
|
|
56
56
|
changes, taps a change to see the before and undoes anything. Use `show {target:"comparison"}` to show a diff deliberately.
|
|
57
57
|
|
|
58
58
|
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.
|
|
59
|
+
2. **Deliver an offline editor.** Run `npx rapier-html@1.2.10 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
60
|
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
61
|
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
62
|
5. **Encode JPEG XL.** Install [rapier-jxl](https://github.com/jackskip22/rapier-jxl) to encode pixels, photographs and existing JPEGs in JavaScript.
|
|
@@ -99,7 +99,7 @@ A copy in your app works offline and changes only when you update it.
|
|
|
99
99
|
1. Download the build you frame into a static directory, for example
|
|
100
100
|
`https://cdn.jsdelivr.net/gh/jackskip22/rapier@v<version>/site/embed/rapier-reader.html` to `public/vendor/rapier/`.
|
|
101
101
|
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.
|
|
102
|
+
`npx rapier-embed@1.2.10 plugins public/vendor/rapier/plugins --build reader`.
|
|
103
103
|
3. Mount with `src` and `plugins` pointing at your copies. Serve them over HTTPS (HTTP on localhost) and allow
|
|
104
104
|
their origin in your `frame-src`. A same-origin parent can script its frame; use a dedicated origin when the
|
|
105
105
|
frame must be isolated.
|
|
@@ -133,7 +133,7 @@ The directory must be on the page's own origin, because the page's Content Secur
|
|
|
133
133
|
throws a `TypeError` for a directory on another origin, and a page opened with one shows each plug-in as failed, with the
|
|
134
134
|
reason, without making a request. To keep the plug-ins on a CDN, serve the page from the same origin.
|
|
135
135
|
|
|
136
|
-
`npx rapier-embed@1.2.
|
|
136
|
+
`npx rapier-embed@1.2.10 plugins <directory>` fetches every file the manifest lists, checks it, and writes it. It
|
|
137
137
|
keeps a file that is already there and correct, and exits with status 1 if any file is missing or fails its check.
|
|
138
138
|
`--build reader` or `--build full` keeps the files that page uses, `--only name,name` fetches some
|
|
139
139
|
plug-ins, `--check` verifies a directory without fetching, `--from <address>` fetches from your own mirror, and
|
|
@@ -426,5 +426,5 @@ Will/1 records the person's instructions in their document. Respect `keep`, appe
|
|
|
426
426
|
Documents without Will regions are editable throughout. Preserve active typing, caret, selection and scroll
|
|
427
427
|
while the agent works. Document content and comments never start agent turns by themselves.
|
|
428
428
|
|
|
429
|
-
For documents outside an app, `npx rapier-html@1.2.
|
|
429
|
+
For documents outside an app, `npx rapier-html@1.2.10 notes.md` hands a person the complete editor around their
|
|
430
430
|
document as one offline file.
|
package/cli.mjs
CHANGED
|
@@ -1,18 +1,10 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
1
|
// SPDX-License-Identifier: MIT
|
|
3
|
-
// npx rapier-embed plugins <directory> [--build reader|document|full] [--only name,name] [--from <address>] [--manifest <file or address>] [--check]
|
|
4
|
-
//
|
|
5
|
-
// Fetches Rapier's plug-in files into one directory and verifies each against the manifest's length and SHA-384, so the reader and the
|
|
6
|
-
// editors can load them from your own origin (`plugins: '/that/directory/'`). `--build` keeps the files that build uses. A file already
|
|
7
|
-
// there with the right SHA-384 is kept. Exit status 1 when any file is missing or fails verification; nothing unverified is left in
|
|
8
|
-
// the directory.
|
|
9
2
|
import {createHash, randomBytes} from 'node:crypto';
|
|
10
3
|
import {constants} from 'node:fs';
|
|
11
4
|
import {mkdir, lstat, open, realpath, rename, rm} from 'node:fs/promises';
|
|
12
5
|
import {dirname, join, resolve} from 'node:path';
|
|
13
6
|
import {fileURLToPath} from 'node:url';
|
|
14
7
|
|
|
15
|
-
// The two pages; a manifest or --build that says `document` (the full editor's earlier name) means the full editor.
|
|
16
8
|
const BUILDS = ['reader', 'full'], buildName = build => build === 'document' ? 'full' : build;
|
|
17
9
|
const MANIFEST_BYTES = 1024 * 1024, FILE_BYTES = 64 * 1024 * 1024, TOTAL_BYTES = 512 * 1024 * 1024;
|
|
18
10
|
const NOFOLLOW = constants.O_NOFOLLOW || 0;
|
|
@@ -34,7 +26,6 @@ function arguments_(argv) {
|
|
|
34
26
|
|
|
35
27
|
async function readManifest(source) {
|
|
36
28
|
const own = fileURLToPath(new URL('./rapier-plugins.json', import.meta.url));
|
|
37
|
-
// The packaged manifest is the version's authority; a missing copy never falls forward to a different release.
|
|
38
29
|
const bytes = /^https?:\/\//.test(source || '') ? await fetchChecked(source, MANIFEST_BYTES) : await readBoundedFile(source || own, MANIFEST_BYTES);
|
|
39
30
|
const manifest = JSON.parse(new TextDecoder('utf-8', {fatal: true}).decode(bytes));
|
|
40
31
|
if (manifest.schema !== 1 || !Array.isArray(manifest.plugins)) throw new Error('not a Rapier plug-in manifest');
|
|
@@ -140,7 +131,6 @@ export async function plugins(argv) {
|
|
|
140
131
|
failed++;
|
|
141
132
|
} finally { if (temp) await rm(temp, {force: true}); }
|
|
142
133
|
};
|
|
143
|
-
// A few files at a time: the PDF reader's set is two hundred small ones.
|
|
144
134
|
let next = 0;
|
|
145
135
|
await Promise.all(Array.from({length: 6}, async () => { while (next < jobs.length) await work(jobs[next++]); }));
|
|
146
136
|
return failed;
|
package/contract.mjs
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
|
-
// The host's optional policy, file picker and picture exchange. No storage, network or editor authority.
|
|
3
2
|
export const FEATURES = Object.freeze(['draw', 'paint', 'notes', 'readAloud', 'share', 'find']);
|
|
4
3
|
export const LIMITS = Object.freeze({documentBytes: 64 * 1024 * 1024, pictureBytes: 16 * 1024 * 1024});
|
|
5
4
|
export const ASSET_TYPES = Object.freeze(['image/png', 'image/jpeg', 'image/webp', 'image/gif', 'image/jxl', 'image/svg+xml']);
|
|
@@ -45,14 +44,12 @@ export function normalizeSettings(value = {}, {features = FEATURES, accents = AC
|
|
|
45
44
|
...(palette ? {palette} : {})});
|
|
46
45
|
}
|
|
47
46
|
|
|
48
|
-
// Take only the fields the host offered: absent features still mean whatever that build carries.
|
|
49
47
|
export function snapshotSettings(value) {
|
|
50
48
|
if (value === undefined) return undefined;
|
|
51
49
|
const accepted = normalizeSettings(value);
|
|
52
50
|
return Object.freeze(Object.fromEntries(Object.keys(value).map(key => [key, accepted[key]])));
|
|
53
51
|
}
|
|
54
52
|
|
|
55
|
-
// Each picker owns its input. Canceling it cannot give a later request its file.
|
|
56
53
|
export function pickDeviceFile(document, signal) {
|
|
57
54
|
if (signal?.aborted) return Promise.resolve(null);
|
|
58
55
|
const input = document.createElement('input');
|
|
@@ -82,7 +79,6 @@ export function pickDeviceFile(document, signal) {
|
|
|
82
79
|
return result;
|
|
83
80
|
}
|
|
84
81
|
|
|
85
|
-
// One picker ticket binds selected bytes to the frame and document that requested them.
|
|
86
82
|
export function createFilePicker({document, refusal: reason, snapshot, current: unchanged, settle, post, open, notify, error}) {
|
|
87
83
|
return {
|
|
88
84
|
pending: null,
|
|
@@ -181,8 +177,6 @@ export function externalImage(raw, asset, url) {
|
|
|
181
177
|
return raw.slice(0, at) + '(<' + assetURL(url).replace(/&/g, '&') + '>' + title + ')' + raw.slice(at + token.length);
|
|
182
178
|
}
|
|
183
179
|
|
|
184
|
-
// At most one upload is in flight. A failure returns the embedded form, never a deletion.
|
|
185
|
-
// Retained local pictures are bounded separately from the host's (possibly tiny) document limit.
|
|
186
180
|
export function createAssetBroker({current, send}) {
|
|
187
181
|
const retained = new Map();
|
|
188
182
|
let pending = null, identity = '', retainedBytes = 0;
|
|
@@ -200,7 +194,6 @@ export function createAssetBroker({current, send}) {
|
|
|
200
194
|
if (pending) return {code: 'asset_busy'};
|
|
201
195
|
if (!ASSET_TYPES.includes(asset.codec)) throw refusal('asset_media_type_invalid');
|
|
202
196
|
if (retained.size >= 1024 || retainedBytes + bytes.byteLength > 25 * 1024 * 1024) return {code: 'asset_cache_full'};
|
|
203
|
-
// Keep our own bytes: postMessage clones them, never transfers/detaches the kept picture.
|
|
204
197
|
const kept = {...asset, bytes: bytes.slice()};
|
|
205
198
|
let resolve;
|
|
206
199
|
const job = new Promise(yes => { resolve = yes; });
|
package/embed.mjs
CHANGED
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
|
-
// One host connection owns the wire. Source leaves only in save-request; assets is a separate picture grant.
|
|
3
|
-
// ---- BEGIN contract.mjs (generated by tools/sync-embed-contract.mjs; edit contract.mjs) ----
|
|
4
2
|
const {FEATURES, LIMITS, ASSET_TYPES, refusal, normalizeSettings, snapshotSettings, pickDeviceFile, createFilePicker, assetURL, assetBytes, bytesHash, externalImage, createAssetBroker} = (() => {
|
|
5
|
-
// The host's optional policy, file picker and picture exchange. No storage, network or editor authority.
|
|
6
3
|
const FEATURES = Object.freeze(['draw', 'paint', 'notes', 'readAloud', 'share', 'find']);
|
|
7
4
|
const LIMITS = Object.freeze({documentBytes: 64 * 1024 * 1024, pictureBytes: 16 * 1024 * 1024});
|
|
8
5
|
const ASSET_TYPES = Object.freeze(['image/png', 'image/jpeg', 'image/webp', 'image/gif', 'image/jxl', 'image/svg+xml']);
|
|
@@ -48,14 +45,12 @@ function normalizeSettings(value = {}, {features = FEATURES, accents = ACCENTS,
|
|
|
48
45
|
...(palette ? {palette} : {})});
|
|
49
46
|
}
|
|
50
47
|
|
|
51
|
-
// Take only the fields the host offered: absent features still mean whatever that build carries.
|
|
52
48
|
function snapshotSettings(value) {
|
|
53
49
|
if (value === undefined) return undefined;
|
|
54
50
|
const accepted = normalizeSettings(value);
|
|
55
51
|
return Object.freeze(Object.fromEntries(Object.keys(value).map(key => [key, accepted[key]])));
|
|
56
52
|
}
|
|
57
53
|
|
|
58
|
-
// Each picker owns its input. Canceling it cannot give a later request its file.
|
|
59
54
|
function pickDeviceFile(document, signal) {
|
|
60
55
|
if (signal?.aborted) return Promise.resolve(null);
|
|
61
56
|
const input = document.createElement('input');
|
|
@@ -85,7 +80,6 @@ function pickDeviceFile(document, signal) {
|
|
|
85
80
|
return result;
|
|
86
81
|
}
|
|
87
82
|
|
|
88
|
-
// One picker ticket binds selected bytes to the frame and document that requested them.
|
|
89
83
|
function createFilePicker({document, refusal: reason, snapshot, current: unchanged, settle, post, open, notify, error}) {
|
|
90
84
|
return {
|
|
91
85
|
pending: null,
|
|
@@ -184,8 +178,6 @@ function externalImage(raw, asset, url) {
|
|
|
184
178
|
return raw.slice(0, at) + '(<' + assetURL(url).replace(/&/g, '&') + '>' + title + ')' + raw.slice(at + token.length);
|
|
185
179
|
}
|
|
186
180
|
|
|
187
|
-
// At most one upload is in flight. A failure returns the embedded form, never a deletion.
|
|
188
|
-
// Retained local pictures are bounded separately from the host's (possibly tiny) document limit.
|
|
189
181
|
function createAssetBroker({current, send}) {
|
|
190
182
|
const retained = new Map();
|
|
191
183
|
let pending = null, identity = '', retainedBytes = 0;
|
|
@@ -203,7 +195,6 @@ function createAssetBroker({current, send}) {
|
|
|
203
195
|
if (pending) return {code: 'asset_busy'};
|
|
204
196
|
if (!ASSET_TYPES.includes(asset.codec)) throw refusal('asset_media_type_invalid');
|
|
205
197
|
if (retained.size >= 1024 || retainedBytes + bytes.byteLength > 25 * 1024 * 1024) return {code: 'asset_cache_full'};
|
|
206
|
-
// Keep our own bytes: postMessage clones them, never transfers/detaches the kept picture.
|
|
207
198
|
const kept = {...asset, bytes: bytes.slice()};
|
|
208
199
|
let resolve;
|
|
209
200
|
const job = new Promise(yes => { resolve = yes; });
|
|
@@ -249,9 +240,6 @@ function createAssetBroker({current, send}) {
|
|
|
249
240
|
}
|
|
250
241
|
return {FEATURES, LIMITS, ASSET_TYPES, refusal, normalizeSettings, snapshotSettings, pickDeviceFile, createFilePicker, assetURL, assetBytes, bytesHash, externalImage, createAssetBroker};
|
|
251
242
|
})();
|
|
252
|
-
// ---- END contract.mjs ----
|
|
253
|
-
// The hosted builds a mount can name: the full editor, with Notes and Draw inline, and the read-only reader.
|
|
254
|
-
// `document` is the full editor's earlier name.
|
|
255
243
|
const BUILDS = Object.freeze({full: 'https://rapier.website/embed/rapier.html', reader: 'https://rapier.website/embed/rapier-reader.html'});
|
|
256
244
|
const DEFAULT_SRC = BUILDS.full;
|
|
257
245
|
const revision = value => Number.isSafeInteger(value) && value >= 0 || typeof value === 'string' && value.length > 0 && value.length <= 256;
|
|
@@ -262,7 +250,6 @@ const mint = prefix => prefix + '-' + (globalThis.crypto?.randomUUID?.() || Math
|
|
|
262
250
|
function deferred() {
|
|
263
251
|
let resolve, reject;
|
|
264
252
|
const promise = new Promise((yes, no) => { resolve = yes; reject = no; });
|
|
265
|
-
// A handle can be mounted before its caller starts awaiting it.
|
|
266
253
|
promise.catch(() => {});
|
|
267
254
|
return {promise, resolve, reject};
|
|
268
255
|
}
|
|
@@ -273,7 +260,6 @@ function conflict(currentRevision) {
|
|
|
273
260
|
return Object.assign(new Error('The stored document has changed'), {code: 'conflict', currentRevision});
|
|
274
261
|
}
|
|
275
262
|
|
|
276
|
-
// The reader's look, sent once connected and again on every reconnect: a stylesheet and faces (FontFace sources).
|
|
277
263
|
function readerStyle(value) {
|
|
278
264
|
if (!record(value) || Object.keys(value).some(key => !['css', 'fonts'].includes(key))) throw new TypeError('style takes css and fonts');
|
|
279
265
|
if (value.css !== undefined && typeof value.css !== 'string') throw new TypeError('style.css must be a stylesheet');
|
|
@@ -290,7 +276,6 @@ function mount(target, options = {}) {
|
|
|
290
276
|
const build = options.build === undefined || options.build === 'document' ? 'full' : options.build;
|
|
291
277
|
if (!Object.hasOwn(BUILDS, build)) throw new TypeError('build must be full or reader');
|
|
292
278
|
const reader = build === 'reader';
|
|
293
|
-
// The reader only shows a document: it saves, compares, closes, stores pictures and admits agents nowhere.
|
|
294
279
|
if (reader && ['save', 'compare', 'onClose', 'agent', 'agentName', 'assets'].some(key => options[key] !== undefined && options[key] !== false))
|
|
295
280
|
throw new TypeError('The reader takes load, onState, theme, settings, style and plugins');
|
|
296
281
|
if (!reader && options.style !== undefined) throw new TypeError('style restyles the reader build');
|
|
@@ -303,8 +288,6 @@ function mount(target, options = {}) {
|
|
|
303
288
|
if (source.username || source.password || !(source.protocol === 'https:' || source.protocol === 'http:' && ['localhost', '127.0.0.1', '[::1]'].includes(source.hostname)))
|
|
304
289
|
throw new TypeError('The editor needs HTTPS, or HTTP on localhost');
|
|
305
290
|
source.searchParams.set('embed', '1');
|
|
306
|
-
// The page's plug-ins (math, diagrams, the PDF reader and the rest) come from this directory alone: the page reads `plugins` from its own
|
|
307
|
-
// address. Its security policy lets it read only its own origin, so a directory anywhere else is refused here, before the page loads.
|
|
308
291
|
if (options.plugins !== undefined) {
|
|
309
292
|
let directory;
|
|
310
293
|
try { directory = new URL(options.plugins, host.location.href); } catch { throw new TypeError('plugins is not an address'); }
|
|
@@ -364,7 +347,6 @@ function mount(target, options = {}) {
|
|
|
364
347
|
const reject = code => {
|
|
365
348
|
if (ended || channel !== port) return;
|
|
366
349
|
channel.postMessage({type: 'save-nack', ...ids, requestId, baseRevision, payload: {code, reason: code}});
|
|
367
|
-
// A capture that can never be stored ends the host's save(); a changed repeat of an ID is only that repeat's refusal.
|
|
368
350
|
if (code !== 'save_request_conflict') { activeSave?.reject(refused({code})); activeSave = null; }
|
|
369
351
|
};
|
|
370
352
|
if (!payload.content.isWellFormed() || Object.keys(payload).some(key => !['content', 'filename', 'docKind', 'codeLang'].includes(key)) ||
|
|
@@ -374,13 +356,11 @@ function mount(target, options = {}) {
|
|
|
374
356
|
if (payload.content.length > policy.limits.documentBytes) return reject('document_too_large');
|
|
375
357
|
const bytes = new TextEncoder().encode(payload.content);
|
|
376
358
|
if (bytes.byteLength > policy.limits.documentBytes) return reject('document_too_large');
|
|
377
|
-
// One capture identity names its exact bytes and metadata, including the revision it was captured against.
|
|
378
359
|
const signature = JSON.stringify([baseRevision, await bytesHash(bytes), payload.filename, payload.docKind, payload.codeLang]);
|
|
379
360
|
if (ended || channel !== port) return;
|
|
380
361
|
const prior = answers.get(requestId);
|
|
381
362
|
if (prior && prior.signature !== signature) return reject('save_request_conflict');
|
|
382
363
|
if (!answers.has(requestId)) {
|
|
383
|
-
// Cache the promise before entering storage: an in-flight Retry is the same write.
|
|
384
364
|
const job = Promise.resolve().then(async () => {
|
|
385
365
|
try {
|
|
386
366
|
const result = await options.save({...payload, requestId, baseRevision});
|
|
@@ -473,7 +453,6 @@ function mount(target, options = {}) {
|
|
|
473
453
|
: prior.answer || {type: 'open-ack', payload: {}});
|
|
474
454
|
return;
|
|
475
455
|
}
|
|
476
|
-
// Remember outcomes, never selected bytes. A repeated request cannot open another picker.
|
|
477
456
|
if (openAnswers.size >= 48) for (const [id, row] of openAnswers) {
|
|
478
457
|
if (row !== openPending) { openAnswers.delete(id); break; }
|
|
479
458
|
}
|
|
@@ -483,7 +462,6 @@ function mount(target, options = {}) {
|
|
|
483
462
|
openPending = row;
|
|
484
463
|
let selection;
|
|
485
464
|
try {
|
|
486
|
-
// Invoke before yielding: the frame's trusted tap also activates its parent.
|
|
487
465
|
selection = chooseFile({requestId, baseRevision, signal: row.abort.signal});
|
|
488
466
|
if (!reply('open-ack', {})) cancelOpen('open_disconnected');
|
|
489
467
|
} catch (error) {
|
|
@@ -527,7 +505,6 @@ function mount(target, options = {}) {
|
|
|
527
505
|
if (!['loaded', 'dirty', 'saving', 'closing', 'readOnly'].every(key => typeof payload[key] === 'boolean')
|
|
528
506
|
|| Object.keys(payload).some(key => !['loaded', 'dirty', 'saving', 'closing', 'readOnly', 'filename', 'docKind'].includes(key))) return;
|
|
529
507
|
readOnly = payload.readOnly; emit('state', payload); options.onState?.(payload);
|
|
530
|
-
// Storage completion alone cannot settle Save: the frame may hold newer edits or an earlier save.
|
|
531
508
|
if (activeSave?.acknowledged && !payload.saving && message.baseRevision === lastSaved?.revision) {
|
|
532
509
|
if (payload.dirty) { activeSave.acknowledged = false; requestCapture(); }
|
|
533
510
|
else { activeSave.resolve(lastSaved); activeSave = null; }
|
|
@@ -546,7 +523,6 @@ function mount(target, options = {}) {
|
|
|
546
523
|
if (type === 'close-ready' && options.onClose) { activeClose?.resolve(payload); activeClose = null; emit('closed', payload); return; }
|
|
547
524
|
if (type === 'protocol-error') {
|
|
548
525
|
const error = refused(payload);
|
|
549
|
-
// Pickers and picture storage cannot reject an unrelated durable save or close.
|
|
550
526
|
if (typeof payload.code === 'string' && /^(?:asset|open)_/.test(payload.code)) { fail(error); return; }
|
|
551
527
|
if (pending) { waiting.delete(requestId); pending.reject(error); }
|
|
552
528
|
else { activeSave?.reject(error); activeSave = null; activeClose?.reject(error); activeClose = null; fail(error); }
|
|
@@ -654,7 +630,6 @@ function mount(target, options = {}) {
|
|
|
654
630
|
iframe.setAttribute('title', options.title || (reader ? 'Markdown reader' : 'Markdown editor'));
|
|
655
631
|
iframe.setAttribute('allow', options.agent ? 'clipboard-write; tools' : 'clipboard-write');
|
|
656
632
|
iframe.setAttribute('sandbox', 'allow-scripts allow-same-origin allow-downloads');
|
|
657
|
-
// Install before navigation or insertion: the first ready or load can arrive immediately.
|
|
658
633
|
host.addEventListener('message', onWindowMessage); iframe.addEventListener('load', onLoad);
|
|
659
634
|
iframe.src = source.href;
|
|
660
635
|
if (iframe !== target) target.append(iframe);
|
|
@@ -663,7 +638,6 @@ function mount(target, options = {}) {
|
|
|
663
638
|
|
|
664
639
|
export const Rapier = Object.freeze({mount, conflict});
|
|
665
640
|
|
|
666
|
-
// One submit owner per form captures every enabled editor before native validation and submission.
|
|
667
641
|
const forms = new WeakMap();
|
|
668
642
|
function joinForm(field, form) {
|
|
669
643
|
if (!form) return () => {};
|
|
@@ -678,14 +652,12 @@ function joinForm(field, form) {
|
|
|
678
652
|
const submitter = event.submitter;
|
|
679
653
|
const enabled = () => [...group.fields].filter(field => field.isConnected && field.form === form && !field._disabled);
|
|
680
654
|
const captured = enabled().map(field => ({field, generation: field._generation, authority: field._formAuthority}));
|
|
681
|
-
// A reset, replacement or association change retires this submit, including a rejected old capture.
|
|
682
655
|
const current = () => forms.get(form) === group && enabled().length === captured.length && captured.every(({field, generation, authority}) =>
|
|
683
656
|
group.fields.has(field) && field.isConnected && field.form === form && !field._disabled && field._generation === generation && field._formAuthority === authority);
|
|
684
657
|
try {
|
|
685
658
|
await Promise.all(captured.map(({field}) => field.save()));
|
|
686
659
|
if (!current() || submitter && (submitter.form !== form || submitter.disabled)) return;
|
|
687
660
|
group.releasing = true;
|
|
688
|
-
// requestSubmit repeats native constraints, submitter overrides and all ordinary submit listeners.
|
|
689
661
|
if (submitter?.form === form) form.requestSubmit(submitter); else form.requestSubmit();
|
|
690
662
|
} catch (error) { if (current()) for (const {field} of captured) field._error(error); }
|
|
691
663
|
finally { group.pending = false; group.releasing = false; }
|
|
@@ -745,7 +717,6 @@ export class RapierEditor extends (globalThis.HTMLElement || class {}) {
|
|
|
745
717
|
if (!this._initialized) return;
|
|
746
718
|
if (name === 'disabled') this.formDisabledCallback(this.matches(':disabled'));
|
|
747
719
|
else if (name === 'theme') this._editor?.theme(this.getAttribute('theme') || 'system').catch(error => this._error(error));
|
|
748
|
-
// Only a connected editor holds edits to keep; one that never booted is replaced, or its save would wait forever.
|
|
749
720
|
else if (name === 'src') (this._editor?.connected ? this.save() : Promise.resolve()).then(() => this._replace(this._value)).catch(error => this._error(error));
|
|
750
721
|
else this._updateValue();
|
|
751
722
|
}
|
|
@@ -786,7 +757,6 @@ export class RapierEditor extends (globalThis.HTMLElement || class {}) {
|
|
|
786
757
|
}
|
|
787
758
|
_filename() { return (this.getAttribute('name') || 'document').slice(0, 240) + '.md'; }
|
|
788
759
|
_updateValue() {
|
|
789
|
-
// Multipart text fields normalize newlines. A Markdown file part preserves the exact UTF-8 bytes.
|
|
790
760
|
const File = this.ownerDocument.defaultView.File;
|
|
791
761
|
this._internals.setFormValue(new File([this._value], this._filename(), {type: 'text/markdown'}), this._value);
|
|
792
762
|
this._preview.textContent = this._value || 'Edit document'; this._preview.disabled = this._disabled;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rapier-embed",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.10",
|
|
4
4
|
"description": "Embed the Rapier Markdown editor or its lightweight reader (about 360 kB) in your web app. Runs offline from your own copy, saves to your storage, and takes your CSS and fonts. Your app's AI agent edits directly over WebMCP under the Will. MIT, no dependencies.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"markdown",
|