@paged-media/plugin-sdk 0.2.40-canary.0 → 0.2.42-canary.0

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/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { PagedBundle, BindingProvider, Disposable, ActiveBindingProvider, BindingPropertyRequest, BindingReadResult, BindingPropertyWrite, BindingWriteResult, BindingCollectionRequest, BindingCollectionResult, MutationInput, FontFaceAsset, BundleHost, ClipboardPayload, ConsentResult, DataProviderRegistration, DataProviderHandle, DataProviderInfo, DataProviderSnapshot, JournalRecord, ShellSurface, WidgetSurface, WillSaveEvent, ToolSettingValue, SchemaPanelRenderer, SchemaPanelContribution, EditContextContribution, ObjectTypeContribution, Diagnostic, TextCaret, PagedEditor, PluginManifest, ToolContribution, PanelContribution, CommandContribution, KeybindingContribution, OverlayContribution, ImporterContribution, ExporterContribution, ToolPreviewShape, BindingsSurface, PanelProps, SchemaGate, WasmArtifact, CanvasPointerEvent, Mutation, MutationOutcome, BindingProviderHandle } from '@paged-media/plugin-api';
1
+ import { PagedBundle, BindingProvider, Disposable, ActiveBindingProvider, BindingPropertyRequest, BindingReadResult, BindingPropertyWrite, BindingWriteResult, BindingCollectionRequest, BindingCollectionResult, MutationInput, FontFaceAsset, BundleHost, ClipboardPayload, ConsentResult, DataProviderRegistration, DataProviderHandle, DataProviderInfo, DataProviderSnapshot, JournalRecord, ShellSurface, WidgetSurface, WillSaveEvent, ToolSettingValue, SchemaPanelRenderer, SchemaPanelContribution, EditContextContribution, ObjectTypeContribution, Diagnostic, TextCaret, OpenDocumentResult, PagedEditor, PluginManifest, ToolContribution, PanelContribution, CommandContribution, KeybindingContribution, OverlayContribution, ImporterContribution, ExporterContribution, ToolPreviewShape, EnteredEditContext, BindingsSurface, PanelProps, SchemaGate, WasmArtifact, CanvasPointerEvent, Mutation, MutationOutcome, BindingProviderHandle } from '@paged-media/plugin-api';
2
2
  import { ComponentType } from 'react';
3
3
 
4
4
  /**
@@ -104,6 +104,18 @@ interface BundleAssetProvider {
104
104
  /** The bytes of a document font face, or `null` when the host has
105
105
  * none. Style-agnostic when `style` is omitted. */
106
106
  getFontFace(family: string, style?: string): Promise<FontFaceAsset | null>;
107
+ /** W-15 — register a face in the engine's SCENE-LAYER face table
108
+ * (`registerFont` with `scope: "sceneLayer"`): scene-layer text
109
+ * resolves it, the document never sees it. The SDK calls it once per
110
+ * `host.assets.registerFont` and again when it replays the live faces
111
+ * after a disposal. Optional: a host without it (with
112
+ * `clearSceneFonts`) leaves `supports("assets.registerFont@1")` false. */
113
+ registerFont?(family: string, bytes: Uint8Array, style?: string): Promise<void>;
114
+ /** W-15 — drop every scene-layer face (`clearFontRegistry` with
115
+ * `scope: "sceneLayer"`), leaving the document's faces alone. The SDK
116
+ * owns which faces are live, so disposing one face is "clear, then
117
+ * register the rest again". */
118
+ clearSceneFonts?(): Promise<void>;
107
119
  }
108
120
  /** Blob-store budgets (K-4 / S-08). The default per-plugin quota when a
109
121
  * manifest requests none; a manifest's `storage.quotaBytes` may only
@@ -240,6 +252,27 @@ interface NativeDocumentBackend {
240
252
  listParts(prefix?: string): Promise<string[]>;
241
253
  open(bytes: Uint8Array): Promise<void>;
242
254
  }
255
+ /**
256
+ * D-26 — the backend the editor injects to back `host.documents`. The SDK
257
+ * owns the capability gate (`capabilities.documents.export` / `.open`); the
258
+ * backend owns the IO AND the unsaved-edits decision: `open` must ask the
259
+ * user (host UI) before discarding a dirty active document and answer
260
+ * `{ opened: false, reason: "declined" }` when they keep it. `requester`
261
+ * is filled by the SDK from the calling bundle's manifest (not from the
262
+ * plugin's arguments), so the prompt can name who is asking. No backend →
263
+ * both doors reject and `supports("documents.open@1")` /
264
+ * `supports("documents.exportPaged@1")` are false.
265
+ */
266
+ interface DocumentsBackend {
267
+ exportPaged(): Promise<Uint8Array>;
268
+ open(bytes: Uint8Array, request: {
269
+ name: string | null;
270
+ requester: {
271
+ id: string;
272
+ name: string;
273
+ };
274
+ }): Promise<OpenDocumentResult>;
275
+ }
243
276
  /** The SHARED cross-plugin data-provider registry (paged.data §7.1 / D-09). The
244
277
  * editor creates ONE (`createDataProviderRegistry`) and injects the SAME
245
278
  * instance into every plugin host, so a provider plugin and a consumer plugin
@@ -402,6 +435,13 @@ interface CreateBundleHostOptions {
402
435
  * holds listeners that never run and `supports("document.onWillSave@1")`
403
436
  * is false. */
404
437
  willSave?: WillSaveBackend;
438
+ /** W-19 — the host's shell fills `pageId` / `pagePoint` /
439
+ * `contentPoint` on the `EnteredEditContext` of a POINTER entry (the
440
+ * double-click, the Type-tool click on owned content). The SDK passes
441
+ * the entered context through untouched either way; this flag only
442
+ * makes `supports("editContext.enterPoint@1")` true, so it is the HOST
443
+ * that vouches for delivery. */
444
+ editContextEnterPoint?: boolean;
405
445
  /** The host's tool-options store. When absent, `host.tools.settings`
406
446
  * answers `{}` and `supports("tools.settings@1")` is false. */
407
447
  toolSettings?: ToolSettingsBackend;
@@ -469,6 +509,12 @@ interface CreateBundleHostOptions {
469
509
  * `supports("document.openNative@1")` answer true. When absent, the reads
470
510
  * answer `null`/`[]` and `open` rejects (the honest no-backend door). */
471
511
  nativeDocument?: NativeDocumentBackend;
512
+ /** D-26 — host-provided DOCUMENTS backend. When present,
513
+ * `host.documents.exportPaged` / `open` go through it (gated on
514
+ * `capabilities.documents.export` / `.open`) and
515
+ * `supports("documents.exportPaged@1")` / `supports("documents.open@1")`
516
+ * answer true. When absent, both doors reject. */
517
+ documents?: DocumentsBackend;
472
518
  /** Host-provided WORKER backend (K-3 / S-07 / I-02). When present,
473
519
  * `host.workers.spawn` resolves a declared bundle-relative module +
474
520
  * constructs a host-owned `Worker` through it (capability-gated,
@@ -521,6 +567,14 @@ interface HeadlessCanvasWorker {
521
567
  handleMessage(input: string): string;
522
568
  loadDocumentDirect(seq: number, bytes: Uint8Array, font?: Uint8Array, cmykIccProfile?: Uint8Array): string;
523
569
  runResolveJson(): string | undefined;
570
+ /** The engine shaper (v38): `{ advance, ascender, descender }` in pt, or
571
+ * null when no document is loaded / the face resolves to nothing.
572
+ * Optional so a structural fake need not carry it. */
573
+ measureText?(family: string, style: string | null | undefined, text: string, sizePt: number): {
574
+ advance: number;
575
+ ascender: number;
576
+ descender: number;
577
+ } | null;
524
578
  free(): void;
525
579
  }
526
580
  interface LoadedEngine {
@@ -580,6 +634,32 @@ interface RecordedContribution {
580
634
  value: ToolContribution | PanelContribution | SchemaPanelContribution | CommandContribution | KeybindingContribution | OverlayContribution | EditContextContribution | ObjectTypeContribution | ImporterContribution | ExporterContribution;
581
635
  }
582
636
  interface HarnessOptions extends LoadHeadlessEngineOptions, Pick<CreateBundleHostOptions, "console" | "storage" | "capabilityMode" | "assetSource" | "blobStore" | "clipboard" | "secrets" | "bindingProviders" | "willSave" | "toolSettings"> {
637
+ /** D-27 — back `host.text` with the ENGINE's shaper
638
+ * (`CanvasWorker.measureText`, the one the editor's worker calls)
639
+ * instead of the estimate, so `measureString` / `measureStrings`
640
+ * answer real widths against the loaded document's fonts and
641
+ * `supports("text.measure@1")` / `("text.measureStrings@1")` are true.
642
+ * Off by default: existing headless suites pinned the estimate. */
643
+ engineShaper?: boolean;
644
+ /** D-26 — the headless stand-in for the editor's keep/discard prompt.
645
+ * `host.documents.open` calls it ONLY when the active document has
646
+ * unsaved edits; resolve `false` to keep them (the door then answers
647
+ * `{ opened: false, reason: "declined" }`). Default: discard. */
648
+ confirmReplace?: (request: {
649
+ name: string | null;
650
+ requester: {
651
+ id: string;
652
+ name: string;
653
+ };
654
+ }) => boolean | Promise<boolean>;
655
+ /** D-28 — the default font every load lays out with, the headless
656
+ * stand-in for the editor's `defaultFontProvider` (the editor passes
657
+ * `/fonts/Inter.ttf`). The engine lays text out, and reports a story
658
+ * `overset`, only when it has a font: without one nothing is laid out and
659
+ * every story reads `overset: false` (not measured). Passed to every
660
+ * load, `load()` and `host.documents.open` alike. Absent: no font, as
661
+ * before. */
662
+ defaultFont?: Uint8Array;
583
663
  }
584
664
  /** A headless will-save registry: listeners register per plugin and
585
665
  * `fire()` awaits them all, the way the editor's save path does — so a
@@ -638,6 +718,19 @@ interface HeadlessHost {
638
718
  * bundle's "geometry AND label at once" claim is assertable without a
639
719
  * browser. `lastToolPreview()` tracks the list's FIRST shape. */
640
720
  lastToolPreviews(): readonly ToolPreviewShape[] | null;
721
+ /** W-20 — every LIVE overlay layer (`host.overlay.layer`), in stack
722
+ * order (bottom-most first), keyed `<manifest id>/<layer id>`, with the
723
+ * shapes it holds now (`[]` once cleared). A disposed layer is gone. */
724
+ overlayLayers(): ReadonlyArray<{
725
+ key: string;
726
+ shapes: readonly ToolPreviewShape[];
727
+ }>;
728
+ /** W-19 — play the SHELL's part on a pointer entry: call the registered
729
+ * edit context's `onEnter` with `ctx` (which may carry `pageId` /
730
+ * `pagePoint` / `contentPoint`, as the editor fills them). The harness
731
+ * advertises `editContext.enterPoint@1`. Returns `false` when no
732
+ * context of `ctx.type` is registered. */
733
+ enterEditContext(ctx: EnteredEditContext): boolean;
641
734
  /** ADR-023 phase A — the SHARED binding-provider registry this host
642
735
  * injected. It is the HOST side of the seam, so a conformance test
643
736
  * plays the part the editor's shared panel plays in phase C: enter
@@ -656,6 +749,13 @@ interface HeadlessHost {
656
749
  /** Load an IDML package into the headless document. Resolves to the
657
750
  * loaded page ids (or throws on a parse failure). */
658
751
  load(idml: Uint8Array): Promise<string[]>;
752
+ /** D-26 — every document a bundle opened through `host.documents.open`,
753
+ * in order (declined opens are not listed). */
754
+ openedDocuments(): ReadonlyArray<{
755
+ name: string | null;
756
+ requester: string;
757
+ pageIds: string[];
758
+ }>;
659
759
  /** Activate a bundle against this host (apiVersion-negotiated). The
660
760
  * returned disposer runs the bundle's teardown; the host's own
661
761
  * facade teardown runs on `dispose()`. */
@@ -921,4 +1021,4 @@ declare function contributeObjectType(host: BundleHost, contribution: ObjectType
921
1021
  */
922
1022
  declare function contributeBindingProvider(host: BundleHost, contextType: string, provider: BindingProvider): BindingProviderHandle;
923
1023
 
924
- export { API_VERSION, ASSET_BUDGETS, BLOB_BUDGETS, type BindingProviderBackend, type BlobStore, type BundleAssetProvider, type BundleAssetSource, type BundleHostHandle, type BundleTrust, CANVAS_WASM_PKG, CLICK_DRAG_THRESHOLD_PX, type ClipboardBackend, type ConsentBackend, type CreateBundleHostOptions, type DataProviderBackend, type DiagnosticsSink, DisposableStore, FALLBACK_WIDGETS, HOST_FEATURES, type HarnessOptions, type HeadlessCanvasWorker, type HeadlessHost, type HeadlessHostHandle, type JournalSink, type LoadBundleWasmOptions, type LoadHeadlessEngineOptions, type LoadedBundle, type LoadedBundleWasm, type LoadedEngine, type NativeDocumentBackend, type PageDrag, PluginApiNotImplemented, PluginCapabilityError, type RecordableAssetSource, type RecordedContribution, type RecordedFontFaceRequest, type RegisteredBindingProvider, type SecretStoreBackend, type SeededFace, type ShellBackend, type SpawnedWorker, type StorageBacking, type TextCaretBackend, type ToolSettingsBackend, WASM_BUDGETS, WORKER_BUDGETS, type WillSaveBackend, type WorkerBackend, beginPageDrag, commitAndSelect, contributeBindingProvider, contributeEditContext, contributeObjectType, contributePanel, contributeSchemaPanel, contributeTool, createBindingProviderRegistry, createBundleHost, createDataProviderRegistry, createHeadlessHost, createRecordableAssetSource, defineBundle, endLocalFor, inMemoryToolSettings, inMemoryWillSave, loadBundle, loadBundleWasm, loadHeadlessEngine, makeSchemaPanelComponent, protocolFromVersion, pxToPt, readVendoredWireVersion, resolveCanvasWasm, resolveGate, satisfiesApiVersion, toDisposable };
1024
+ export { API_VERSION, ASSET_BUDGETS, BLOB_BUDGETS, type BindingProviderBackend, type BlobStore, type BundleAssetProvider, type BundleAssetSource, type BundleHostHandle, type BundleTrust, CANVAS_WASM_PKG, CLICK_DRAG_THRESHOLD_PX, type ClipboardBackend, type ConsentBackend, type CreateBundleHostOptions, type DataProviderBackend, type DiagnosticsSink, DisposableStore, type DocumentsBackend, FALLBACK_WIDGETS, HOST_FEATURES, type HarnessOptions, type HeadlessCanvasWorker, type HeadlessHost, type HeadlessHostHandle, type JournalSink, type LoadBundleWasmOptions, type LoadHeadlessEngineOptions, type LoadedBundle, type LoadedBundleWasm, type LoadedEngine, type NativeDocumentBackend, type PageDrag, PluginApiNotImplemented, PluginCapabilityError, type RecordableAssetSource, type RecordedContribution, type RecordedFontFaceRequest, type RegisteredBindingProvider, type SecretStoreBackend, type SeededFace, type ShellBackend, type SpawnedWorker, type StorageBacking, type TextCaretBackend, type ToolSettingsBackend, WASM_BUDGETS, WORKER_BUDGETS, type WillSaveBackend, type WorkerBackend, beginPageDrag, commitAndSelect, contributeBindingProvider, contributeEditContext, contributeObjectType, contributePanel, contributeSchemaPanel, contributeTool, createBindingProviderRegistry, createBundleHost, createDataProviderRegistry, createHeadlessHost, createRecordableAssetSource, defineBundle, endLocalFor, inMemoryToolSettings, inMemoryWillSave, loadBundle, loadBundleWasm, loadHeadlessEngine, makeSchemaPanelComponent, protocolFromVersion, pxToPt, readVendoredWireVersion, resolveCanvasWasm, resolveGate, satisfiesApiVersion, toDisposable };
package/dist/index.js CHANGED
@@ -228,8 +228,17 @@ var HOST_FEATURES = [
228
228
  // needed — DESIGN.md §4.3d).
229
229
  "document.parentOf@1",
230
230
  "document.onDidChange@1",
231
+ // W-22 — the document-opened event. STATIC: it is the client's
232
+ // `documentLoaded` broadcast filtered, and every host posts that on
233
+ // every load (File ▸ Open, File ▸ New, a native-document open).
234
+ "document.onDidOpen@1",
231
235
  "document.getMetadata@1",
232
236
  "document.setMetadata@1",
237
+ // Protocol 69 — the document-scoped label. Static like the snap door:
238
+ // the flag says this SDK forwards the two calls; an engine older than 69
239
+ // answers `null` on read (no `pluginMetadata` in its DocumentMeta) and
240
+ // `applied: false` on write (it does not know the op).
241
+ "document.documentMetadata@1",
233
242
  "selection@1",
234
243
  "viewport@1",
235
244
  "overlay.toolPreview@1",
@@ -328,6 +337,15 @@ var ASSET_BUDGETS = {
328
337
  /** Largest font face the door will serve, in bytes (8 MiB). */
329
338
  maxFontFaceBytes: 8 * 1024 * 1024
330
339
  };
340
+ var sceneFaceLedgers = /* @__PURE__ */ new WeakMap();
341
+ function sceneFaceLedger(provider) {
342
+ let ledger = sceneFaceLedgers.get(provider);
343
+ if (!ledger) {
344
+ ledger = { entries: [], queue: Promise.resolve() };
345
+ sceneFaceLedgers.set(provider, ledger);
346
+ }
347
+ return ledger;
348
+ }
331
349
  var BLOB_BUDGETS = {
332
350
  /** Default per-plugin blob ceiling, in bytes (64 MiB). */
333
351
  defaultQuotaBytes: 64 * 1024 * 1024
@@ -628,6 +646,7 @@ function createBundleHost(getEditor, manifest, options) {
628
646
  const hasWorkers = () => typeof caps?.workers?.max === "number";
629
647
  const hasSecrets = () => caps?.secrets?.sources === true;
630
648
  const hasGpu = () => caps?.gpu?.realm === "bundle";
649
+ const hasDocuments = (dir) => caps?.documents?.[dir] === true;
631
650
  const lists = (arr, id) => arr?.includes(id) ?? false;
632
651
  const declaresType = (arr, type) => arr?.some((e) => e.type === type) ?? false;
633
652
  const requireDeclared = (ok, door, missing) => {
@@ -1098,7 +1117,7 @@ function createBundleHost(getEditor, manifest, options) {
1098
1117
  }
1099
1118
  };
1100
1119
  const foreignMetadataKey = (m) => {
1101
- if (m.op === "setPluginMetadata") {
1120
+ if (m.op === "setPluginMetadata" || m.op === "setDocumentMetadata" || m.op === "setPageMetadata") {
1102
1121
  return m.args.key === metadataKey(manifest) ? null : m.args.key;
1103
1122
  }
1104
1123
  if (m.op === "batch") {
@@ -1147,7 +1166,7 @@ function createBundleHost(getEditor, manifest, options) {
1147
1166
  if (denied !== null) return { applied: false, error: denied };
1148
1167
  const foreign = foreignMetadataKey(mutation);
1149
1168
  if (foreign !== null) {
1150
- const error = `setPluginMetadata key "${foreign}" is outside this plugin's namespace ("${metadataKey(manifest)}")`;
1169
+ const error = `metadata key "${foreign}" is outside this plugin's namespace ("${metadataKey(manifest)}")`;
1151
1170
  log.warn(error);
1152
1171
  return { applied: false, error };
1153
1172
  }
@@ -1384,6 +1403,28 @@ function createBundleHost(getEditor, manifest, options) {
1384
1403
  }
1385
1404
  });
1386
1405
  },
1406
+ async getDocumentMetadata() {
1407
+ requireDocRead("document.getDocumentMetadata");
1408
+ const key = metadataKey(manifest);
1409
+ const meta = await getEditor().client.documentMeta();
1410
+ const entry = meta.pluginMetadata?.find((e) => e.key === key);
1411
+ if (!entry) return null;
1412
+ try {
1413
+ return JSON.parse(entry.value);
1414
+ } catch {
1415
+ return null;
1416
+ }
1417
+ },
1418
+ async setDocumentMetadata(envelope) {
1419
+ return this.mutate({
1420
+ op: "setDocumentMetadata",
1421
+ args: {
1422
+ key: metadataKey(manifest),
1423
+ value: envelope === null ? null : JSON.stringify(envelope),
1424
+ caller: manifest.id
1425
+ }
1426
+ });
1427
+ },
1387
1428
  async frameChain(storyId) {
1388
1429
  requireDocRead("document.frameChain");
1389
1430
  const reply = await getEditor().client.send({
@@ -1414,6 +1455,28 @@ function createBundleHost(getEditor, manifest, options) {
1414
1455
  });
1415
1456
  return store.add(toDisposable(off));
1416
1457
  },
1458
+ // W-22 — the raw `documentLoaded` broadcast, narrowed to a clonable
1459
+ // snapshot (the handle's stats / ruler guides stay host-side).
1460
+ onDidOpen(listener) {
1461
+ requireDocRead("document.onDidOpen");
1462
+ const off = getEditor().client.subscribe((msg) => {
1463
+ if (msg.kind !== "documentLoaded") return;
1464
+ const h2 = msg.payload;
1465
+ try {
1466
+ listener({
1467
+ docId: h2.docId,
1468
+ pageCount: h2.pageCount,
1469
+ pageIds: [...h2.pageIds],
1470
+ pageSizesPt: h2.pageSizesPt.map(
1471
+ ([w, hgt]) => [w, hgt]
1472
+ )
1473
+ });
1474
+ } catch (err) {
1475
+ log.error(`document.onDidOpen listener failed: ${String(err)}`);
1476
+ }
1477
+ });
1478
+ return store.add(toDisposable(off));
1479
+ },
1417
1480
  // Protocol 66 batch — the save hook. The host awaits every
1418
1481
  // registered listener before it serialises the document; the wrapper
1419
1482
  // makes a bundle's failure a logged event, never a failed save.
@@ -1487,8 +1550,30 @@ function createBundleHost(getEditor, manifest, options) {
1487
1550
  // "no reader").
1488
1551
  caret() {
1489
1552
  return options?.textCaret?.read() ?? null;
1553
+ },
1554
+ // D-27 — the batched measure. One round-trip when the editor wires
1555
+ // `measureMany`; otherwise the same answers one call at a time (or
1556
+ // the estimate), so a bundle never needs two code paths.
1557
+ async measureStrings(family, style, texts, sizePt) {
1558
+ if (texts.length === 0) return [];
1559
+ const editorText = getEditor().text;
1560
+ if (editorText?.measureMany) {
1561
+ const out = await editorText.measureMany(family, style, texts, sizePt);
1562
+ if (out.length !== texts.length) {
1563
+ throw new Error(
1564
+ `host.text.measureStrings: the host answered ${out.length} metrics for ${texts.length} strings`
1565
+ );
1566
+ }
1567
+ return out;
1568
+ }
1569
+ return Promise.all(
1570
+ texts.map((t) => text.measureString(family, style, t, sizePt))
1571
+ );
1490
1572
  }
1491
1573
  };
1574
+ let overlayLayerSeq = 0;
1575
+ let overlayLayerWarned = false;
1576
+ const liveOverlayLayers = /* @__PURE__ */ new Set();
1492
1577
  const overlay = {
1493
1578
  setToolPreview(shape) {
1494
1579
  requireDeclared(
@@ -1518,6 +1603,56 @@ function createBundleHost(getEditor, manifest, options) {
1518
1603
  return;
1519
1604
  }
1520
1605
  signals.setToolPreview(list ? list[0] : null);
1606
+ },
1607
+ // W-20 — retained per-bundle layers. Each layer is a host-wide key
1608
+ // `<manifest id>/<layer id>`; registering it with `[]` at creation
1609
+ // fixes its place in the stack (creation order), so a later `set`
1610
+ // never reorders it. Without a host sink the handle is inert (warned
1611
+ // once per bundle) — never a throw, never a write into the shared
1612
+ // tool-preview slot, which would erase the active tool's preview.
1613
+ layer(id) {
1614
+ requireDeclared(
1615
+ hasRendering("overlay"),
1616
+ "overlay.layer",
1617
+ 'capabilities.rendering must include "overlay"'
1618
+ );
1619
+ const layerId = id ?? `layer-${++overlayLayerSeq}`;
1620
+ if (liveOverlayLayers.has(layerId)) {
1621
+ throw new Error(
1622
+ `host.overlay.layer("${layerId}"): a layer with this id is already live in ${manifest.id} \u2014 dispose it first or pick another id`
1623
+ );
1624
+ }
1625
+ const key = `${manifest.id}/${layerId}`;
1626
+ const signals = getEditor().overlaySignals;
1627
+ const sink2 = typeof signals?.setOverlayLayer === "function" && typeof signals?.removeOverlayLayer === "function" ? signals : null;
1628
+ if (!sink2 && !overlayLayerWarned) {
1629
+ overlayLayerWarned = true;
1630
+ log.warn(
1631
+ 'host.overlay.layer \u2014 this host renders no overlay layers (supports("overlay.layers@1") is false); shapes are dropped'
1632
+ );
1633
+ }
1634
+ liveOverlayLayers.add(layerId);
1635
+ sink2?.setOverlayLayer(key, []);
1636
+ let disposed = false;
1637
+ const handle = {
1638
+ id: layerId,
1639
+ set(shapes) {
1640
+ if (disposed) return;
1641
+ sink2?.setOverlayLayer(key, [...shapes]);
1642
+ },
1643
+ clear() {
1644
+ if (disposed) return;
1645
+ sink2?.setOverlayLayer(key, []);
1646
+ },
1647
+ dispose() {
1648
+ if (disposed) return;
1649
+ disposed = true;
1650
+ liveOverlayLayers.delete(layerId);
1651
+ sink2?.removeOverlayLayer(key);
1652
+ }
1653
+ };
1654
+ store.add(handle);
1655
+ return handle;
1521
1656
  }
1522
1657
  };
1523
1658
  const backing = options?.storage ?? defaultStorageBacking();
@@ -1782,6 +1917,65 @@ function createBundleHost(getEditor, manifest, options) {
1782
1917
  }
1783
1918
  return face;
1784
1919
  },
1920
+ // W-15 — the one WRITE: a scene-layer face. The engine has a single
1921
+ // scene table that `clearFontRegistry` can only empty as a whole, so
1922
+ // the SDK keeps the list of live faces per provider (across bundles)
1923
+ // and disposes one face by clearing and replaying the others. Every
1924
+ // engine call goes through the ledger's queue, so a replay never
1925
+ // interleaves with a registration.
1926
+ async registerFont(bytes, family, style) {
1927
+ requireDeclared(
1928
+ hasAsset("fonts"),
1929
+ "assets.registerFont",
1930
+ 'capabilities.assets must include "fonts"'
1931
+ );
1932
+ const label = `assets.registerFont("${family}"${style ? `, "${style}"` : ""})`;
1933
+ if (bytes.byteLength > ASSET_BUDGETS.maxFontFaceBytes) {
1934
+ throw new Error(
1935
+ `${label}: ${bytes.byteLength} bytes is over the ${ASSET_BUDGETS.maxFontFaceBytes}-byte per-face cap`
1936
+ );
1937
+ }
1938
+ const provider = assetSource;
1939
+ const register = provider?.registerFont?.bind(provider);
1940
+ const clear = provider?.clearSceneFonts?.bind(provider);
1941
+ if (!provider || !register || !clear) {
1942
+ log.warn(
1943
+ `${label} ignored \u2014 the host cannot register scene faces (probe supports("assets.registerFont@1")); scene text keeps its fallback face`
1944
+ );
1945
+ return toDisposable(() => {
1946
+ });
1947
+ }
1948
+ const ledger = sceneFaceLedger(provider);
1949
+ const entry = style === void 0 ? { family, bytes: bytes.slice() } : { family, style, bytes: bytes.slice() };
1950
+ const run = (job) => {
1951
+ const next = ledger.queue.then(job);
1952
+ ledger.queue = next.catch(() => {
1953
+ });
1954
+ return next;
1955
+ };
1956
+ await run(async () => {
1957
+ await register(entry.family, entry.bytes, entry.style);
1958
+ ledger.entries.push(entry);
1959
+ });
1960
+ let disposed = false;
1961
+ return store.add(
1962
+ toDisposable(() => {
1963
+ if (disposed) return;
1964
+ disposed = true;
1965
+ void run(async () => {
1966
+ const i = ledger.entries.indexOf(entry);
1967
+ if (i < 0) return;
1968
+ ledger.entries.splice(i, 1);
1969
+ await clear();
1970
+ for (const live of ledger.entries) {
1971
+ await register(live.family, live.bytes, live.style);
1972
+ }
1973
+ }).catch((error) => {
1974
+ log.warn(`${label}: dropping the face failed: ${String(error)}`);
1975
+ });
1976
+ })
1977
+ );
1978
+ },
1785
1979
  // C-5 / I-04 (core v42): a placed DOCUMENT image's ORIGINAL bytes,
1786
1980
  // straight from the engine's resolver/parse cache through the
1787
1981
  // `requestPlacedAssetBytes` wire query — no injected source needed
@@ -2058,6 +2252,42 @@ function createBundleHost(getEditor, manifest, options) {
2058
2252
  return nativeDocBackend.open(bytes);
2059
2253
  }
2060
2254
  };
2255
+ const documentsBackend = options?.documents;
2256
+ const documents = {
2257
+ async exportPaged() {
2258
+ requireDeclared(
2259
+ hasDocuments("export"),
2260
+ "documents.exportPaged",
2261
+ "capabilities.documents.export must be declared"
2262
+ );
2263
+ if (!documentsBackend) {
2264
+ throw new Error(
2265
+ 'host.documents.exportPaged: the host wired no documents backend (supports("documents.exportPaged@1") is false)'
2266
+ );
2267
+ }
2268
+ return documentsBackend.exportPaged();
2269
+ },
2270
+ async open(bytes, opts) {
2271
+ requireDeclared(
2272
+ hasDocuments("open"),
2273
+ "documents.open",
2274
+ "capabilities.documents.open must be declared"
2275
+ );
2276
+ if (!documentsBackend) {
2277
+ throw new Error(
2278
+ 'host.documents.open: the host wired no documents backend (supports("documents.open@1") is false)'
2279
+ );
2280
+ }
2281
+ if (!(bytes instanceof Uint8Array) || bytes.byteLength === 0) {
2282
+ throw new Error("host.documents.open: bytes must be a non-empty Uint8Array");
2283
+ }
2284
+ const name = typeof opts?.name === "string" && opts.name.trim() !== "" ? opts.name.trim() : null;
2285
+ return documentsBackend.open(bytes.slice(), {
2286
+ name,
2287
+ requester: { id: manifest.id, name: manifest.name }
2288
+ });
2289
+ }
2290
+ };
2061
2291
  const clipboardBackend = options?.clipboard;
2062
2292
  const clipboardGate = (door) => {
2063
2293
  const grant = clipboardGrant();
@@ -2263,6 +2493,9 @@ function createBundleHost(getEditor, manifest, options) {
2263
2493
  if (getEditor().text) {
2264
2494
  featureSet.add("text.measure@1");
2265
2495
  }
2496
+ if (getEditor().text?.measureMany) {
2497
+ featureSet.add("text.measureStrings@1");
2498
+ }
2266
2499
  if (getEditor().sceneLayers) {
2267
2500
  featureSet.add("rendering.sceneLayer@1");
2268
2501
  featureSet.add("rendering.sceneLayer.faces@1");
@@ -2270,6 +2503,12 @@ function createBundleHost(getEditor, manifest, options) {
2270
2503
  if (typeof getEditor().overlaySignals?.setToolPreviews === "function") {
2271
2504
  featureSet.add("overlay.multiPreview@1");
2272
2505
  }
2506
+ if (typeof getEditor().overlaySignals?.setOverlayLayer === "function" && typeof getEditor().overlaySignals?.removeOverlayLayer === "function") {
2507
+ featureSet.add("overlay.layers@1");
2508
+ }
2509
+ if (options?.editContextEnterPoint === true) {
2510
+ featureSet.add("editContext.enterPoint@1");
2511
+ }
2273
2512
  if (getEditor().images) {
2274
2513
  featureSet.add("rendering.resourceProvider@1");
2275
2514
  }
@@ -2313,6 +2552,9 @@ function createBundleHost(getEditor, manifest, options) {
2313
2552
  if (options?.assetSource) {
2314
2553
  featureSet.add("assets.fonts@1");
2315
2554
  }
2555
+ if (typeof options?.assetSource?.registerFont === "function" && typeof options?.assetSource?.clearSceneFonts === "function") {
2556
+ featureSet.add("assets.registerFont@1");
2557
+ }
2316
2558
  if (options?.consent) {
2317
2559
  featureSet.add("network.consent@1");
2318
2560
  }
@@ -2335,6 +2577,10 @@ function createBundleHost(getEditor, manifest, options) {
2335
2577
  featureSet.add("document.readNative@1");
2336
2578
  featureSet.add("document.openNative@1");
2337
2579
  }
2580
+ if (options?.documents) {
2581
+ featureSet.add("documents.exportPaged@1");
2582
+ featureSet.add("documents.open@1");
2583
+ }
2338
2584
  if (options?.workers) {
2339
2585
  featureSet.add("workers@1");
2340
2586
  }
@@ -2358,6 +2604,7 @@ function createBundleHost(getEditor, manifest, options) {
2358
2604
  blob,
2359
2605
  parts,
2360
2606
  nativeDocument,
2607
+ documents,
2361
2608
  network,
2362
2609
  dataProviders,
2363
2610
  diagnostics,
@@ -2612,7 +2859,7 @@ function inMemorySecretStore() {
2612
2859
  };
2613
2860
  }
2614
2861
  var seqCounter = 1;
2615
- function makeEngineEditor(worker, recorder, onToolPreview, onToolPreviews) {
2862
+ function makeEngineEditor(worker, recorder, onToolPreview, onToolPreviews, overlayLayers, engineShaper = false) {
2616
2863
  const protocol = worker.protocolVersion;
2617
2864
  const listeners = /* @__PURE__ */ new Set();
2618
2865
  const fanOut = (reply) => {
@@ -2762,6 +3009,15 @@ function makeEngineEditor(worker, recorder, onToolPreview, onToolPreviews) {
2762
3009
  toolPreview = toolPreviews ? toolPreviews[0] : null;
2763
3010
  onToolPreviews(toolPreviews);
2764
3011
  onToolPreview(toolPreview);
3012
+ },
3013
+ // W-20 — the retained layers. A Map keeps first-insertion order,
3014
+ // which is the stack order the editor renders; a re-set keeps the
3015
+ // key's place.
3016
+ setOverlayLayer(key, shapes) {
3017
+ overlayLayers.set(key, shapes);
3018
+ },
3019
+ removeOverlayLayer(key) {
3020
+ overlayLayers.delete(key);
2765
3021
  }
2766
3022
  },
2767
3023
  // No tool spine + no content caret headlessly — both are inert
@@ -2772,7 +3028,23 @@ function makeEngineEditor(worker, recorder, onToolPreview, onToolPreviews) {
2772
3028
  },
2773
3029
  contentSelection: { contentSelection: null }
2774
3030
  };
2775
- return editor;
3031
+ const measureText = worker.measureText?.bind(worker);
3032
+ if (engineShaper && measureText) {
3033
+ const zero = { advance: 0, ascender: 0, descender: 0 };
3034
+ const one = (family, style, t, sizePt) => {
3035
+ const m = measureText(family, style, t, sizePt);
3036
+ return m ? { advance: m.advance, ascender: m.ascender, descender: m.descender } : zero;
3037
+ };
3038
+ editor.text = {
3039
+ async measure(family, style, t, sizePt) {
3040
+ return one(family, style, t, sizePt);
3041
+ },
3042
+ async measureMany(family, style, texts, sizePt) {
3043
+ return texts.map((t) => one(family, style, t, sizePt));
3044
+ }
3045
+ };
3046
+ }
3047
+ return { editor, fanOut };
2776
3048
  }
2777
3049
  async function createHeadlessHost(options = {}) {
2778
3050
  const engine = await loadHeadlessEngine(options);
@@ -2780,7 +3052,8 @@ async function createHeadlessHost(options = {}) {
2780
3052
  const contributions = [];
2781
3053
  let lastPreview = null;
2782
3054
  let lastPreviews = null;
2783
- const editor = makeEngineEditor(
3055
+ const overlayLayerMap = /* @__PURE__ */ new Map();
3056
+ const { editor, fanOut } = makeEngineEditor(
2784
3057
  worker,
2785
3058
  contributions,
2786
3059
  (value) => {
@@ -2788,8 +3061,55 @@ async function createHeadlessHost(options = {}) {
2788
3061
  },
2789
3062
  (value) => {
2790
3063
  lastPreviews = value;
2791
- }
3064
+ },
3065
+ overlayLayerMap,
3066
+ options.engineShaper === true
2792
3067
  );
3068
+ const loadDirect = (bytes) => {
3069
+ const reply = JSON.parse(
3070
+ options.defaultFont ? worker.loadDocumentDirect(seqCounter++, bytes, options.defaultFont) : worker.loadDocumentDirect(seqCounter++, bytes)
3071
+ );
3072
+ if (reply.kind === "documentLoaded") {
3073
+ try {
3074
+ worker.runResolveJson();
3075
+ } catch {
3076
+ }
3077
+ }
3078
+ return reply;
3079
+ };
3080
+ const loadFailure = (reply) => {
3081
+ const errKind = reply.kind === "loadFailed" ? reply.payload.error.kind : reply.kind;
3082
+ return new Error(`headless load failed (${reply.kind}: ${errKind})`);
3083
+ };
3084
+ const opened = [];
3085
+ const documentsBackend = {
3086
+ async exportPaged() {
3087
+ const reply = await editor.client.send({ kind: "exportPaged", payload: {} });
3088
+ if (reply.kind === "pagedExported") return Uint8Array.from(reply.payload.bytes);
3089
+ if (reply.kind === "pagedPartFailed") throw new Error(reply.payload.error);
3090
+ throw new Error(`unexpected reply: ${reply.kind}`);
3091
+ },
3092
+ async open(bytes, request) {
3093
+ let dirty = false;
3094
+ try {
3095
+ dirty = (await editor.client.documentMeta()).dirty;
3096
+ } catch {
3097
+ dirty = false;
3098
+ }
3099
+ if (dirty && options.confirmReplace && !await options.confirmReplace(request)) {
3100
+ return { opened: false, reason: "declined" };
3101
+ }
3102
+ const reply = loadDirect(bytes);
3103
+ if (reply.kind !== "documentLoaded") throw loadFailure(reply);
3104
+ fanOut(reply);
3105
+ opened.push({
3106
+ name: request.name,
3107
+ requester: request.requester.id,
3108
+ pageIds: reply.payload.pageIds
3109
+ });
3110
+ return { opened: true, pageIds: reply.payload.pageIds };
3111
+ }
3112
+ };
2793
3113
  let active = null;
2794
3114
  let currentHost = null;
2795
3115
  let disposed = false;
@@ -2814,6 +3134,10 @@ async function createHeadlessHost(options = {}) {
2814
3134
  // headlessly (the editor's real adapter currently serves null;
2815
3135
  // DESIGN.md §13.4). Absent → the no-bytes door.
2816
3136
  assetSource: options.assetSource,
3137
+ // W-19 — `enterEditContext()` below delivers the entering point the
3138
+ // way the editor shell does, so the harness vouches for it.
3139
+ editContextEnterPoint: true,
3140
+ documents: documentsBackend,
2817
3141
  // Record the SCHEMA verbatim at registration — the panel registry
2818
3142
  // only ever sees the synthesized React panel, so the conformance
2819
3143
  // log gets the schema through this adapter seam (no host renderer
@@ -2875,7 +3199,8 @@ async function createHeadlessHost(options = {}) {
2875
3199
  rendering: ["overlay", "hitTest", "sceneLayer"],
2876
3200
  keybindings: true,
2877
3201
  storage: { blob: true },
2878
- secrets: { sources: true }
3202
+ secrets: { sources: true },
3203
+ documents: { export: true, open: true }
2879
3204
  },
2880
3205
  // Broad contribution declarations so the neutral DRIVER host (which
2881
3206
  // registers arbitrary contributions directly in 'warn' mode) never
@@ -2927,21 +3252,30 @@ async function createHeadlessHost(options = {}) {
2927
3252
  lastToolPreviews() {
2928
3253
  return lastPreviews;
2929
3254
  },
3255
+ overlayLayers() {
3256
+ return [...overlayLayerMap].map(([key, shapes]) => ({ key, shapes }));
3257
+ },
3258
+ enterEditContext(ctx) {
3259
+ const entry = contributions.find(
3260
+ (c) => c.kind === "editContext" && c.id === ctx.type
3261
+ );
3262
+ if (!entry) return false;
3263
+ entry.value.onEnter?.(ctx);
3264
+ return true;
3265
+ },
2930
3266
  bindingProviders,
2931
3267
  willSave,
2932
3268
  toolSettings,
2933
3269
  async load(idml) {
2934
- const raw = worker.loadDocumentDirect(seqCounter++, idml);
2935
- const reply = JSON.parse(raw);
3270
+ const reply = loadDirect(idml);
2936
3271
  if (reply.kind === "documentLoaded") {
2937
- try {
2938
- worker.runResolveJson();
2939
- } catch {
2940
- }
3272
+ fanOut(reply);
2941
3273
  return reply.payload.pageIds;
2942
3274
  }
2943
- const errKind = reply.kind === "loadFailed" ? reply.payload.error.kind : reply.kind;
2944
- throw new Error(`headless load failed (${reply.kind}: ${errKind})`);
3275
+ throw loadFailure(reply);
3276
+ },
3277
+ openedDocuments() {
3278
+ return opened;
2945
3279
  },
2946
3280
  loadBundle(bundle) {
2947
3281
  if (active) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@paged-media/plugin-sdk",
3
- "version": "0.2.40-canary.0",
3
+ "version": "0.2.42-canary.0",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -11,7 +11,7 @@
11
11
  }
12
12
  },
13
13
  "dependencies": {
14
- "@paged-media/plugin-api": "0.2.40-canary.0"
14
+ "@paged-media/plugin-api": "0.2.42-canary.0"
15
15
  },
16
16
  "peerDependencies": {
17
17
  "react": "^18.3.0"
@@ -22,7 +22,7 @@
22
22
  }
23
23
  },
24
24
  "devDependencies": {
25
- "@paged-media/canvas-wasm": "0.68.0",
25
+ "@paged-media/canvas-wasm": "0.70.0",
26
26
  "@types/node": "20.19.39",
27
27
  "@types/react": "^18.3.31",
28
28
  "react": "^18.3.1",