rapier-embed 1.1.87 → 1.1.89
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 +259 -106
- package/cli.mjs +95 -0
- package/embed.mjs +42 -3
- package/package.json +13 -6
- package/rapier-plugins.json +1591 -0
package/embed.mjs
CHANGED
|
@@ -146,7 +146,9 @@ function createAssetBroker({current, send}) {
|
|
|
146
146
|
return {FEATURES, LIMITS, ASSET_TYPES, refusal, normalizeSettings, snapshotSettings, assetURL, assetBytes, bytesHash, externalImage, createAssetBroker};
|
|
147
147
|
})();
|
|
148
148
|
// ---- END contract.mjs ----
|
|
149
|
-
|
|
149
|
+
// The hosted builds a mount can name: the editor without Draw and Paint, and the read-only reader.
|
|
150
|
+
const BUILDS = Object.freeze({document: 'https://rapier.website/embed/rapier-document.html', reader: 'https://rapier.website/embed/rapier-reader.html'});
|
|
151
|
+
const DEFAULT_SRC = BUILDS.document;
|
|
150
152
|
const revision = value => Number.isSafeInteger(value) && value >= 0 || typeof value === 'string' && value.length > 0 && value.length <= 256;
|
|
151
153
|
const identifier = value => typeof value === 'string' && value.length > 0 && value.length <= 256;
|
|
152
154
|
const record = value => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
@@ -166,14 +168,46 @@ function conflict(currentRevision) {
|
|
|
166
168
|
return Object.assign(new Error('The stored document has changed'), {code: 'conflict', currentRevision});
|
|
167
169
|
}
|
|
168
170
|
|
|
171
|
+
// The reader's look, sent once connected and again on every reconnect: a stylesheet and faces (FontFace sources).
|
|
172
|
+
function readerStyle(value) {
|
|
173
|
+
if (!record(value) || Object.keys(value).some(key => !['css', 'fonts'].includes(key))) throw new TypeError('style takes css and fonts');
|
|
174
|
+
if (value.css !== undefined && typeof value.css !== 'string') throw new TypeError('style.css must be a stylesheet');
|
|
175
|
+
const face = row => record(row) && typeof row.family === 'string' && row.family.length > 0 &&
|
|
176
|
+
(typeof row.source === 'string' || row.source instanceof ArrayBuffer || ArrayBuffer.isView(row.source)) &&
|
|
177
|
+
(row.descriptors === undefined || record(row.descriptors)) && Object.keys(row).every(key => ['family', 'source', 'descriptors'].includes(key));
|
|
178
|
+
if (value.fonts !== undefined && (!Array.isArray(value.fonts) || !value.fonts.every(face))) throw new TypeError('style.fonts takes {family, source, descriptors?} faces');
|
|
179
|
+
return {...(value.css !== undefined ? {css: value.css} : {}), ...(value.fonts !== undefined ? {fonts: value.fonts.map(row => ({...row}))} : {})};
|
|
180
|
+
}
|
|
181
|
+
|
|
169
182
|
function mount(target, options = {}) {
|
|
170
183
|
const document = target?.ownerDocument;
|
|
171
184
|
if (!document) throw new TypeError('Mount needs an iframe or a container element');
|
|
185
|
+
const build = options.build ?? 'document';
|
|
186
|
+
if (!Object.hasOwn(BUILDS, build)) throw new TypeError('build must be document or reader');
|
|
187
|
+
const reader = build === 'reader';
|
|
188
|
+
// The reader only shows a document: it saves, compares, closes, stores pictures and admits agents nowhere.
|
|
189
|
+
if (reader && ['save', 'compare', 'onClose', 'agent', 'agentName', 'assets'].some(key => options[key] !== undefined && options[key] !== false))
|
|
190
|
+
throw new TypeError('The reader takes load, onState, theme, settings, style and plugins');
|
|
191
|
+
if (!reader && options.style !== undefined) throw new TypeError('style restyles the reader build');
|
|
192
|
+
if (options.plugins !== undefined && (typeof options.plugins !== 'string' || !options.plugins || options.plugins.length > 2048 || /[\u0000-\u001f\u007f]/.test(options.plugins)))
|
|
193
|
+
throw new TypeError('plugins is the address of the directory that holds the plug-in files');
|
|
194
|
+
let currentStyle = options.style === undefined ? undefined : readerStyle(options.style);
|
|
172
195
|
const host = document.defaultView, iframe = target.tagName === 'IFRAME' ? target : document.createElement('iframe');
|
|
173
|
-
const source = new URL(options.src ||
|
|
196
|
+
const source = new URL(options.src || BUILDS[build], host.location.href);
|
|
174
197
|
if (source.username || source.password || !(source.protocol === 'https:' || source.protocol === 'http:' && ['localhost', '127.0.0.1', '[::1]'].includes(source.hostname)))
|
|
175
198
|
throw new TypeError('The editor needs HTTPS, or HTTP on localhost');
|
|
176
199
|
source.searchParams.set('embed', '1');
|
|
200
|
+
// The page's plug-ins (maths, diagrams, the PDF reader and the rest) come from this directory alone: the page reads `plugins` from its own
|
|
201
|
+
// address. Its security policy lets it read only its own origin, so a directory anywhere else is refused here, before the page loads.
|
|
202
|
+
if (options.plugins !== undefined) {
|
|
203
|
+
let directory;
|
|
204
|
+
try { directory = new URL(options.plugins, host.location.href); } catch { throw new TypeError('plugins is not an address'); }
|
|
205
|
+
if (!(directory.protocol === 'https:' || directory.protocol === 'http:') || directory.username || directory.password || directory.search || directory.hash)
|
|
206
|
+
throw new TypeError('plugins is an http or https address of a directory, with no query or fragment');
|
|
207
|
+
if (directory.origin !== source.origin)
|
|
208
|
+
throw new TypeError('plugins must be on ' + source.origin + ', the origin of the page it configures: the page reads plug-ins only from its own origin. Serve the page and its plug-ins together (set src), or leave plugins out');
|
|
209
|
+
source.searchParams.set('plugins', directory.href);
|
|
210
|
+
}
|
|
177
211
|
const origin = source.origin, sessionId = options.sessionId ?? mint('session'), documentId = options.documentId ?? mint('document');
|
|
178
212
|
if (!identifier(sessionId) || !identifier(documentId)) throw new TypeError('Session and document IDs must be nonempty strings of at most 256 code units');
|
|
179
213
|
const agentName = options.agentName;
|
|
@@ -333,6 +367,7 @@ function mount(target, options = {}) {
|
|
|
333
367
|
}
|
|
334
368
|
function prepareBoot() {
|
|
335
369
|
boot = connectedWait.promise.then(async () => {
|
|
370
|
+
if (currentStyle) await send('style', currentStyle, undefined, true);
|
|
336
371
|
if (options.load !== undefined) {
|
|
337
372
|
const value = typeof options.load === 'function' ? await options.load() : options.load;
|
|
338
373
|
if (!record(value) || typeof value.content !== 'string' || !revision(value.revision)) throw new TypeError('load needs content and its revision');
|
|
@@ -394,6 +429,10 @@ function mount(target, options = {}) {
|
|
|
394
429
|
if (!themeValue(value)) throw new TypeError('Theme must be light, dark or system');
|
|
395
430
|
currentTheme = value; await boot; return send('theme', {theme: value}, undefined, true);
|
|
396
431
|
},
|
|
432
|
+
async style(value) {
|
|
433
|
+
if (!reader) throw new TypeError('style restyles the reader build');
|
|
434
|
+
currentStyle = readerStyle(value); await boot; return send('style', currentStyle, undefined, true);
|
|
435
|
+
},
|
|
397
436
|
disconnect() {
|
|
398
437
|
if (ended) return;
|
|
399
438
|
if (connected) send('disconnect', undefined, undefined, true).catch(() => {});
|
|
@@ -411,7 +450,7 @@ function mount(target, options = {}) {
|
|
|
411
450
|
},
|
|
412
451
|
});
|
|
413
452
|
prepareBoot();
|
|
414
|
-
iframe.setAttribute('title', options.title || 'Markdown editor');
|
|
453
|
+
iframe.setAttribute('title', options.title || (reader ? 'Markdown reader' : 'Markdown editor'));
|
|
415
454
|
iframe.setAttribute('allow', options.agent ? 'clipboard-write; tools' : 'clipboard-write');
|
|
416
455
|
iframe.setAttribute('sandbox', 'allow-scripts allow-same-origin allow-downloads');
|
|
417
456
|
// Install before navigation or insertion: the first ready or load can arrive immediately.
|
package/package.json
CHANGED
|
@@ -1,21 +1,23 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rapier-embed",
|
|
3
|
-
"version": "1.1.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.1.89",
|
|
4
|
+
"description": "Embed the Rapier Markdown editor or its lightweight reader (about 190 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 over WebMCP, under the person's review. MIT, no dependencies.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"markdown",
|
|
7
|
-
"editor",
|
|
8
7
|
"markdown-editor",
|
|
8
|
+
"markdown-reader",
|
|
9
|
+
"markdown-viewer",
|
|
9
10
|
"embed",
|
|
10
11
|
"iframe",
|
|
11
12
|
"form-field",
|
|
12
13
|
"web-component",
|
|
13
14
|
"custom-element",
|
|
14
|
-
"
|
|
15
|
-
"offline",
|
|
15
|
+
"offline-first",
|
|
16
16
|
"self-hosted",
|
|
17
|
-
"
|
|
17
|
+
"mobile-first",
|
|
18
|
+
"ai-agents",
|
|
18
19
|
"webmcp",
|
|
20
|
+
"mermaid",
|
|
19
21
|
"review",
|
|
20
22
|
"rapier"
|
|
21
23
|
],
|
|
@@ -34,9 +36,14 @@
|
|
|
34
36
|
"exports": {
|
|
35
37
|
".": "./embed.mjs"
|
|
36
38
|
},
|
|
39
|
+
"bin": {
|
|
40
|
+
"rapier-embed": "./cli.mjs"
|
|
41
|
+
},
|
|
37
42
|
"files": [
|
|
38
43
|
"embed.mjs",
|
|
39
44
|
"contract.mjs",
|
|
45
|
+
"cli.mjs",
|
|
46
|
+
"rapier-plugins.json",
|
|
40
47
|
"README.md",
|
|
41
48
|
"LICENSE"
|
|
42
49
|
],
|