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 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; the two builds it frames are AGPL-3.0-only, and a commercial license is
14
- available (`LICENSING.md` in the Rapier repository).
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.8
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.8 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)).
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.8 plugins public/vendor/rapier/plugins --build reader`.
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.8 plugins <directory>` fetches every file the manifest lists, checks it, and writes it. It
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.8 notes.md` hands a person the complete editor around their
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, '&amp;') + '>' + 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, '&amp;') + '>' + 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.8",
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",