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/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
- const DEFAULT_SRC = 'https://rapier.website/embed/rapier-document.html';
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 || DEFAULT_SRC, host.location.href);
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.87",
4
- "description": "Put a Markdown document editor in your web app as an iframe or an exact Markdown form field, saving to your own storage; agent review events carry no source. Session settings and optional host-stored pictures. MIT, no dependencies.",
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
- "document",
15
- "offline",
15
+ "offline-first",
16
16
  "self-hosted",
17
- "agent",
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
  ],