@openpresentation/opf-editor 0.10.0 → 0.10.1
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 +14 -2
- package/dist/canvas.d.ts +37 -0
- package/dist/canvas.js +107 -15
- package/dist/font-gate.js +107 -0
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Unfinished prepared shaping work is preserved in the [September 15 roadmap](docs/roadmap-shaping-20260915.md); it is not part of the published runtime.
|
|
4
4
|
|
|
5
|
-
Version 0.10.0 requires `@openpresentation/opf` ^0.11.2, renderer ^0.11.0, and PPTX ^0.11.0 (install them together so cover centering resolves the same core everywhere; renderer 0.11.0 also activates the playground's automatic script fonts and lazy Intos fonts). Version 0.9.0 required `@openpresentation/opf` ^0.11.1, renderer ^0.10.0, and PPTX ^0.10.0. Composition and slide transfer fall back to the shared `aptos` font scheme and report unknown scheme ids through `onDiagnostic`; gallery apply keeps every font-scheme role.
|
|
5
|
+
Version 0.10.1 adds the opt-in font gate (`createFontGate`, FF-41) and keeps the 0.10.0 requirements; develop and test it against renderer 0.11.2 and PPTX 0.11.1. Version 0.10.0 requires `@openpresentation/opf` ^0.11.2, renderer ^0.11.0, and PPTX ^0.11.0 (install them together so cover centering resolves the same core everywhere; renderer 0.11.0 also activates the playground's automatic script fonts and lazy Intos fonts). Version 0.9.0 required `@openpresentation/opf` ^0.11.1, renderer ^0.10.0, and PPTX ^0.10.0. Composition and slide transfer fall back to the shared `aptos` font scheme and report unknown scheme ids through `onDiagnostic`; gallery apply keeps every font-scheme role.
|
|
6
6
|
|
|
7
7
|
Version 0.8.0 required `@openpresentation/opf` ^0.11.0, renderer ^0.9.0, and PPTX ^0.9.0. Named ColorRefs (scheme slots, roles, and `var:<id>`) paint through the published renderer and hex-resolve on export. Payload ids share the slide id namespace for pagination and transfer. Native `schemeClr`, theme write, and `p:hf` remain out of scope.
|
|
8
8
|
|
|
@@ -74,6 +74,18 @@ await canvas.ready;
|
|
|
74
74
|
|
|
75
75
|
Load the same font bytes into the browser using `loadBrowserFontRegistry` from `@openpresentation/opf-render/fonts-browser` before mounting. The host owns font URLs, storage and collaboration. For non-Latin documents, pass the pinned script pack's location as `scriptBaseUrl` and call `fonts.ensureScripts(document)` after edits (renderer with `scripts: 'auto'`, FF-19): only the faces for the scripts a document draws are fetched, once, hash-verified, for example Noto Sans JP for Japanese; a Latin-only document fetches none. The playground does this from `./script-fonts/`, which `npm run build:playground` fills with the pinned faces. `onDraft` provides live document drafts; the session only changes on commit. Escape cancels. Concurrent edits to the selected payload cancel a stale draft.
|
|
76
76
|
|
|
77
|
+
Fonts load before pixels (FF-41). A document can need faces the browser registry has not loaded yet: script faces for the languages it draws and vendored preview faces for the font families it resolves (Intos for Aptos, Open Sans, Barlow). Pass a font gate as `fonts` and the canvas never renders such a document early: on every path (editor changes, undo and redo, imports, dimension switches, slide changes, in-progress edits) it shows "Loading fonts…", loads the faces and then renders. A load that fails is reported through `onFonts`/`onError` and offers a retry button; nothing retries by itself, and the failed document is not drawn.
|
|
78
|
+
|
|
79
|
+
```js
|
|
80
|
+
import { createCanvasEditor, createFontGate } from '@openpresentation/opf-editor/canvas';
|
|
81
|
+
|
|
82
|
+
const fonts = createFontGate(fontRegistry); // a registry without the lazy loaders gates nothing
|
|
83
|
+
const canvas = createCanvasEditor(container, { document, fonts, renderOptions: { textMeasurement: fontRegistry.textMeasurement } });
|
|
84
|
+
// Other renders of your own: fonts.run(document, { ready: draw, failed: showError, loading: showSpinner })
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Note that `session.composeSlide()` and `session.paginateSlide()` measure with the plain `textMeasurement` you pass, which is strict: for a document with a script the design font lacks (Japanese under Aptos) pass `createScriptTextMeasurement(fontRegistry.textMeasurement, resolveScriptFonts(document, { slideIndex }))` from `@openpresentation/opf-render/fonts`, as `renderSvg` does internally.
|
|
88
|
+
|
|
77
89
|
These APIs were introduced in 0.1.0. Version 0.7.0 requires core 0.10.0 and renderer 0.8.0 for the canvas, including shared accepted geometry, styled/merged cells, rich table values, headers and content-aware row heights. See the OPF repository’s `docs/live-editor.md` for setup, the support matrix and roadmap. `pnpm pack:ecosystem` in that repository also prepares local preview tarballs for coordinated development.
|
|
78
90
|
|
|
79
91
|
The canvas retains canonical SVG glyphs while a transparent native input supplies the caret. Advanced shaping, freeform object positioning, all chart/media treatments, and cross-engine pixel identity remain work in progress. The Source dialog in the playground now provides a live JSON preview; changes are validated before committing.
|
|
@@ -247,7 +259,7 @@ Public npm package publication is handled by `.github/workflows/release.yml` thr
|
|
|
247
259
|
|
|
248
260
|
The current checkout uses `@openpresentation/opf/composition` for portable geometry. Slides can select `auto`, `row`, `column`, or `grid`, set weighted tracks, and request path-specific overflow diagnostics. See the sibling OPF repo's `docs/dynamic-composition.md` for the complete contract.
|
|
249
261
|
|
|
250
|
-
Version 0.10.
|
|
262
|
+
Version 0.10.1 requires `@openpresentation/opf@^0.11.2`. The optional renderer peer requires `@openpresentation/opf-render@^0.11.0` (the font gate gates nothing on a registry without the lazy loaders). Development and coordinated playground export use renderer 0.11.2 and `@openpresentation/opf-pptx@^0.11.1`. Clean registry installs support the composition APIs without sibling checkouts. For coordinated source development, build OPF and run `node scripts/link-ecosystem.mjs` there; `pnpm test:ecosystem` verifies shared geometry and import/export behavior.
|
|
251
263
|
|
|
252
264
|
## Local interactive demo
|
|
253
265
|
|
package/dist/canvas.d.ts
CHANGED
|
@@ -1,6 +1,35 @@
|
|
|
1
1
|
import type { EditorSession } from "./index.js";
|
|
2
2
|
import type { RenderSvgOptions } from "@openpresentation/opf-render";
|
|
3
3
|
import type { SlideComposition } from "@openpresentation/opf/composition";
|
|
4
|
+
/** Faces a document needs (script packages and vendored files) that are not loaded yet, and the way to load them. */
|
|
5
|
+
export interface FontGate {
|
|
6
|
+
/** Synchronous. Empty means the document can render now. Never throws. */
|
|
7
|
+
pending(document: unknown): string[];
|
|
8
|
+
/** Load every pending face (vendored faces first, then script faces). Rejects with a `fonts-unavailable` error; never retries by itself. */
|
|
9
|
+
ensure(document: unknown, options?: { signal?: AbortSignal }): Promise<void>;
|
|
10
|
+
/** `whenFontsReady` on this gate. */
|
|
11
|
+
run(document: unknown, handlers?: FontGateHandlers): Promise<void>;
|
|
12
|
+
}
|
|
13
|
+
export interface FontGateHandlers {
|
|
14
|
+
/** Return false once the document was superseded; the late result is then dropped. */
|
|
15
|
+
isCurrent?: () => boolean;
|
|
16
|
+
loading?: (pending: string[]) => void;
|
|
17
|
+
ready?: () => void;
|
|
18
|
+
failed?: (error: Error) => void;
|
|
19
|
+
}
|
|
20
|
+
export declare const FONTS_PENDING: "fonts-pending";
|
|
21
|
+
export declare const FONTS_UNAVAILABLE: "fonts-unavailable";
|
|
22
|
+
/** Wrap a browser font registry (`loadBrowserFontRegistry`). Registries without the lazy loaders gate nothing. */
|
|
23
|
+
export declare function createFontGate(registry: object): FontGate;
|
|
24
|
+
/**
|
|
25
|
+
* Ensure fonts for a document, then run `ready` (synchronously when nothing is pending). The one
|
|
26
|
+
* "ensure fonts, then render" helper: load failures and exceptions from `ready` go to `failed`.
|
|
27
|
+
*/
|
|
28
|
+
export declare function whenFontsReady(
|
|
29
|
+
gate: Pick<FontGate, "pending" | "ensure"> | undefined,
|
|
30
|
+
document: unknown,
|
|
31
|
+
handlers?: FontGateHandlers,
|
|
32
|
+
): Promise<void>;
|
|
4
33
|
export interface CanvasEditorOptions {
|
|
5
34
|
editor?: EditorSession;
|
|
6
35
|
document?: unknown;
|
|
@@ -8,6 +37,12 @@ export interface CanvasEditorOptions {
|
|
|
8
37
|
/** Show keyboard-accessible dividers for resizing composition tracks. */
|
|
9
38
|
layoutEditing?: boolean;
|
|
10
39
|
renderOptions?: RenderSvgOptions;
|
|
40
|
+
/**
|
|
41
|
+
* A font gate (`createFontGate(registry)`). With one, the canvas never renders a document whose faces are still
|
|
42
|
+
* loading: it shows "Loading fonts…", loads them and then renders, on every path (editor changes, undo/redo,
|
|
43
|
+
* imports, dimension switches, slide changes, drafts). Without one every document renders at once.
|
|
44
|
+
*/
|
|
45
|
+
fonts?: Pick<FontGate, "pending" | "ensure">;
|
|
11
46
|
/** Optional empty host for property forms; defaults to a floating canvas panel. */
|
|
12
47
|
propertiesContainer?: HTMLElement;
|
|
13
48
|
onSelect?: (selection: {
|
|
@@ -24,6 +59,8 @@ export interface CanvasEditorOptions {
|
|
|
24
59
|
onCommit?: (event: { path: string; editor: EditorSession }) => void;
|
|
25
60
|
onCancel?: (event: { path?: string }) => void;
|
|
26
61
|
onError?: (error: Error) => void;
|
|
62
|
+
/** Font loading progress of the canvas: `loading`, `error` (a face could not be loaded; the canvas offers a retry) and `ready`. */
|
|
63
|
+
onFonts?: (event: { state: "loading"; pending: string[] } | { state: "error"; error: Error } | { state: "ready" }) => void;
|
|
27
64
|
onRender?: (event: {
|
|
28
65
|
document: unknown;
|
|
29
66
|
slideIndex: number;
|
package/dist/canvas.js
CHANGED
|
@@ -19,7 +19,9 @@ import { createBlockControls } from "./block-controls.js";
|
|
|
19
19
|
import { createLayoutHandles } from "./layout-handles.js";
|
|
20
20
|
import { createRichTextInput } from "./rich-text-input.js";
|
|
21
21
|
import { createRichTextToolbar } from "./rich-text-toolbar.js";
|
|
22
|
+
import { FONTS_PENDING, fontsPendingError, whenFontsReady } from "./font-gate.js";
|
|
22
23
|
export { getEditableFields } from "./canvas-fields.js";
|
|
24
|
+
export { createFontGate, whenFontsReady, FONTS_PENDING, FONTS_UNAVAILABLE } from "./font-gate.js";
|
|
23
25
|
|
|
24
26
|
/** Allocated placeholder or internal-part bounds for the selection outline, not glyph ink. */
|
|
25
27
|
export function allocatedSelectionBox(node, item) {
|
|
@@ -62,8 +64,13 @@ export function createCanvasEditor(container, options = {}) {
|
|
|
62
64
|
const editor =
|
|
63
65
|
options.editor ??
|
|
64
66
|
createEditorSession(options.document, { rejectInvalid: true });
|
|
67
|
+
// `options.fonts` is a font gate (createFontGate): a document whose faces are not loaded yet is never rendered. The canvas
|
|
68
|
+
// loads them first and shows "Loading fonts…" meanwhile; without a gate every document renders at once, as before.
|
|
69
|
+
const fonts = options.fonts;
|
|
65
70
|
let slideIndex = options.slideIndex ?? 0,
|
|
66
71
|
renderOptions = options.renderOptions ?? {},
|
|
72
|
+
showToken = 0,
|
|
73
|
+
fontsShown = false,
|
|
67
74
|
active = null,
|
|
68
75
|
selectedPath = null,
|
|
69
76
|
disposed = false,
|
|
@@ -100,12 +107,12 @@ export function createCanvasEditor(container, options = {}) {
|
|
|
100
107
|
onTyping: beginEdit,
|
|
101
108
|
onProperties(path) { if (commit()) openProperties(path, editor.get(path)); },
|
|
102
109
|
validateChange(path, value) {
|
|
103
|
-
|
|
110
|
+
validateRender(createCanvasDraft(editor.document, path, value));
|
|
104
111
|
},
|
|
105
112
|
onChange(path) { clearNotice(); options.onCommit?.({path, editor}); }
|
|
106
113
|
});
|
|
107
114
|
const layoutHandles = createLayoutHandles(root, {
|
|
108
|
-
editor, enabled: options.layoutEditing, render, beforeEdit: commit,
|
|
115
|
+
editor, enabled: options.layoutEditing, render: renderFor, beforeEdit: commit,
|
|
109
116
|
isTextEditing: () => !!active,
|
|
110
117
|
clearError: clearNotice, onError: report,
|
|
111
118
|
onDraft: value => options.onDraft?.(value),
|
|
@@ -113,9 +120,9 @@ export function createCanvasEditor(container, options = {}) {
|
|
|
113
120
|
onCancel: value => { clearNotice(); options.onCancel?.(value); },
|
|
114
121
|
});
|
|
115
122
|
const blockControls = createBlockControls(root, {
|
|
116
|
-
editor, render, beforeEdit: () => {if(!commit())return false;richToolbar.hide();return true;}, enabled: () => layoutHandles.enabled,
|
|
123
|
+
editor, render: renderFor, beforeEdit: () => {if(!commit())return false;richToolbar.hide();return true;}, enabled: () => layoutHandles.enabled,
|
|
117
124
|
isEditing: () => !!active || !!layoutHandles.editingPath, slideIndex: () => slideIndex,
|
|
118
|
-
validate: document =>
|
|
125
|
+
validate: document => validateRender(document),
|
|
119
126
|
clearError: clearNotice, onError: report,
|
|
120
127
|
onMove: path => choose(splitOpfPath(path).join(".")),
|
|
121
128
|
onCommit: value => options.onCommit?.(value),
|
|
@@ -142,8 +149,75 @@ export function createCanvasEditor(container, options = {}) {
|
|
|
142
149
|
editor,
|
|
143
150
|
});
|
|
144
151
|
}
|
|
152
|
+
const pendingMessage = "Loading fonts for this document…";
|
|
153
|
+
// A synchronous render or validation of a document whose faces are still loading fails with a clear "fonts-pending"
|
|
154
|
+
// error and starts the load, so the next attempt succeeds. It never draws glyphs the registry cannot provide.
|
|
155
|
+
function requireFonts(document) {
|
|
156
|
+
const pending = fonts?.pending(document) ?? [];
|
|
157
|
+
if (!pending.length) return;
|
|
158
|
+
notice.hidden = false;
|
|
159
|
+
notice.textContent = pendingMessage;
|
|
160
|
+
fonts.ensure(document).then(
|
|
161
|
+
() => { if (!disposed && notice.textContent === pendingMessage) clearNotice(); },
|
|
162
|
+
(error) => { if (!disposed) report(error); },
|
|
163
|
+
);
|
|
164
|
+
throw fontsPendingError(pending);
|
|
165
|
+
}
|
|
166
|
+
function validateRender(document) {
|
|
167
|
+
requireFonts(document);
|
|
168
|
+
return renderSvg(document, {...renderOptions, slideIndex});
|
|
169
|
+
}
|
|
170
|
+
function fontsState(kind, detail) {
|
|
171
|
+
fontsShown = true;
|
|
172
|
+
const size = preview.firstElementChild?.getBoundingClientRect?.().height || 0;
|
|
173
|
+
layoutHandles.hide();
|
|
174
|
+
blockControls.hide();
|
|
175
|
+
richToolbar.hide();
|
|
176
|
+
const box = doc.createElement("div");
|
|
177
|
+
box.className = "opf-canvas-fonts";
|
|
178
|
+
box.setAttribute("role", kind === "error" ? "alert" : "status");
|
|
179
|
+
box.style.cssText = `box-sizing:border-box;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:10px;min-height:${Math.max(160, Math.round(size))}px;padding:16px;text-align:center;font:14px/1.4 system-ui;background:rgba(127,127,127,.12);border-radius:4px`;
|
|
180
|
+
const message = doc.createElement("span");
|
|
181
|
+
message.textContent = kind === "error" ? detail.message : "Loading fonts…";
|
|
182
|
+
box.append(message);
|
|
183
|
+
if (kind === "error") {
|
|
184
|
+
const retry = doc.createElement("button");
|
|
185
|
+
retry.type = "button";
|
|
186
|
+
retry.textContent = "Retry loading fonts";
|
|
187
|
+
retry.addEventListener("click", () => show());
|
|
188
|
+
box.append(retry);
|
|
189
|
+
}
|
|
190
|
+
preview.replaceChildren(box);
|
|
191
|
+
if (kind === "error") options.onError?.(detail);
|
|
192
|
+
options.onFonts?.(kind === "error" ? { state: "error", error: detail } : { state: "loading", pending: detail });
|
|
193
|
+
}
|
|
194
|
+
// Render a document the canvas did not draw itself (an editor change, a slide or option change) once its fonts are loaded.
|
|
195
|
+
function show(target) {
|
|
196
|
+
if (disposed) return;
|
|
197
|
+
const token = ++showToken;
|
|
198
|
+
return whenFontsReady(fonts, target ?? editor.document, {
|
|
199
|
+
isCurrent: () => !disposed && token === showToken,
|
|
200
|
+
loading: (pending) => fontsState("loading", pending),
|
|
201
|
+
ready: () => {
|
|
202
|
+
try {
|
|
203
|
+
render(target);
|
|
204
|
+
} catch (error) {
|
|
205
|
+
if (error?.code === FONTS_PENDING) show(target);
|
|
206
|
+
else report(error);
|
|
207
|
+
}
|
|
208
|
+
},
|
|
209
|
+
failed: (error) => fontsState("error", error),
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
// Renders now when the document's fonts are loaded (errors throw to the caller), otherwise after loading them.
|
|
213
|
+
function renderFor(document) {
|
|
214
|
+
if (fonts?.pending(document ?? editor.document).length) show(document);
|
|
215
|
+
else render(document);
|
|
216
|
+
}
|
|
145
217
|
function render(document = editor.document) {
|
|
146
218
|
if (disposed) return;
|
|
219
|
+
requireFonts(document);
|
|
220
|
+
showToken++;
|
|
147
221
|
const slides = document.slides ?? [];
|
|
148
222
|
slideIndex = Math.max(0, Math.min(slideIndex, slides.length - 1));
|
|
149
223
|
const svgText = renderSvg(document, {
|
|
@@ -233,6 +307,10 @@ export function createCanvasEditor(container, options = {}) {
|
|
|
233
307
|
blockControls.update(document, geometry);
|
|
234
308
|
if (active?.kind === "text") positionInput();
|
|
235
309
|
if (active?.kind === "rich-text") active.rich?.update();
|
|
310
|
+
if (fontsShown) {
|
|
311
|
+
fontsShown = false;
|
|
312
|
+
options.onFonts?.({ state: "ready" });
|
|
313
|
+
}
|
|
236
314
|
options.onRender?.({
|
|
237
315
|
document,
|
|
238
316
|
slideIndex,
|
|
@@ -241,6 +319,22 @@ export function createCanvasEditor(container, options = {}) {
|
|
|
241
319
|
draft: !!active || !!layoutHandles.editingPath,
|
|
242
320
|
});
|
|
243
321
|
}
|
|
322
|
+
// An in-progress edit whose text needs faces that are not loaded yet waits for them, then draws again.
|
|
323
|
+
function deferDraft(draft) {
|
|
324
|
+
if (!fonts?.pending(draft).length) return false;
|
|
325
|
+
active.valid = false;
|
|
326
|
+
notice.hidden = false;
|
|
327
|
+
notice.textContent = pendingMessage;
|
|
328
|
+
fonts.ensure(draft).then(
|
|
329
|
+
() => {
|
|
330
|
+
if (disposed) return;
|
|
331
|
+
if (notice.textContent === pendingMessage) clearNotice();
|
|
332
|
+
if (active) queueDraft();
|
|
333
|
+
},
|
|
334
|
+
(error) => { if (!disposed) report(error); },
|
|
335
|
+
);
|
|
336
|
+
return true;
|
|
337
|
+
}
|
|
244
338
|
function queueDraft() {
|
|
245
339
|
if (frame) win.cancelAnimationFrame(frame);
|
|
246
340
|
frame = win.requestAnimationFrame(() => {
|
|
@@ -249,6 +343,7 @@ export function createCanvasEditor(container, options = {}) {
|
|
|
249
343
|
if (active.kind === "properties") {
|
|
250
344
|
try {
|
|
251
345
|
const draft = propertyDraft(active);
|
|
346
|
+
if (deferDraft(draft)) return;
|
|
252
347
|
clearNotice();
|
|
253
348
|
render(draft);
|
|
254
349
|
options.onDraft?.({
|
|
@@ -264,6 +359,7 @@ export function createCanvasEditor(container, options = {}) {
|
|
|
264
359
|
try {
|
|
265
360
|
const value = active.kind === "rich-text" ? active.rich.value : parseCanvasValue(active.input.value, active.type, active.originalValue);
|
|
266
361
|
const draft = createCanvasDraft(editor.document, active.path, value);
|
|
362
|
+
if (deferDraft(draft)) return;
|
|
267
363
|
clearNotice();
|
|
268
364
|
active.valid = true;
|
|
269
365
|
active.draft = draft;
|
|
@@ -659,7 +755,7 @@ export function createCanvasEditor(container, options = {}) {
|
|
|
659
755
|
value = parseCanvasValue(edit.input.value, edit.type, edit.originalValue);
|
|
660
756
|
else value = propertyDraft(edit);
|
|
661
757
|
if (edit.kind === "text" || edit.kind === "rich-text")
|
|
662
|
-
|
|
758
|
+
validateRender(createCanvasDraft(editor.document, edit.path, value));
|
|
663
759
|
committing = true;
|
|
664
760
|
if ((edit.kind === "text" || edit.kind === "rich-text") && JSON.stringify(value) !== edit.base)
|
|
665
761
|
editor.set(edit.path, value, { source: "canvas", rejectInvalid: true });
|
|
@@ -725,11 +821,7 @@ export function createCanvasEditor(container, options = {}) {
|
|
|
725
821
|
queueDraft();
|
|
726
822
|
return;
|
|
727
823
|
}
|
|
728
|
-
|
|
729
|
-
render();
|
|
730
|
-
} catch (error) {
|
|
731
|
-
report(error);
|
|
732
|
-
}
|
|
824
|
+
show();
|
|
733
825
|
});
|
|
734
826
|
const resize = new win.ResizeObserver(() => {
|
|
735
827
|
if (active?.kind === "text") positionInput();
|
|
@@ -748,7 +840,7 @@ export function createCanvasEditor(container, options = {}) {
|
|
|
748
840
|
}
|
|
749
841
|
});
|
|
750
842
|
try {
|
|
751
|
-
|
|
843
|
+
renderFor();
|
|
752
844
|
} catch (error) {
|
|
753
845
|
unsubscribe();
|
|
754
846
|
resize.disconnect();
|
|
@@ -759,7 +851,7 @@ export function createCanvasEditor(container, options = {}) {
|
|
|
759
851
|
throw error;
|
|
760
852
|
}
|
|
761
853
|
const ready = Promise.resolve(doc.fonts?.ready).then(() => {
|
|
762
|
-
if (!disposed && !active)
|
|
854
|
+
if (!disposed && !active) show();
|
|
763
855
|
});
|
|
764
856
|
return {
|
|
765
857
|
editor,
|
|
@@ -795,21 +887,21 @@ export function createCanvasEditor(container, options = {}) {
|
|
|
795
887
|
if (!Number.isInteger(index) || !editor.document.slides?.[index])
|
|
796
888
|
throw new RangeError("Slide index is out of range.");
|
|
797
889
|
if (index === slideIndex) {
|
|
798
|
-
if (!active)
|
|
890
|
+
if (!active) renderFor();
|
|
799
891
|
return true;
|
|
800
892
|
}
|
|
801
893
|
if (!commit()) return false;
|
|
802
894
|
richToolbar.hide();
|
|
803
895
|
slideIndex = index;
|
|
804
896
|
selectedPath = null;
|
|
805
|
-
|
|
897
|
+
renderFor();
|
|
806
898
|
return true;
|
|
807
899
|
},
|
|
808
900
|
setRenderOptions(next) {
|
|
809
901
|
if (!commit()) return false;
|
|
810
902
|
richToolbar.hide();
|
|
811
903
|
renderOptions = next;
|
|
812
|
-
|
|
904
|
+
renderFor();
|
|
813
905
|
return true;
|
|
814
906
|
},
|
|
815
907
|
destroy() {
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// Fonts before pixels. A document can need faces the browser font registry has not loaded yet: script faces for the
|
|
2
|
+
// languages it draws (Japanese, Arabic, ...) and vendored preview faces for the font families it resolves (Intos for
|
|
3
|
+
// Aptos, Open Sans, Barlow, ...). The renderer cannot draw such a document until those faces are loaded (a deck that
|
|
4
|
+
// sets Aptos and contains Japanese fails with "Intos Display cannot display U+65E5"), so nothing may render or measure
|
|
5
|
+
// a new document before `ensure` resolves. This module is the one gate every document-replacing path goes through.
|
|
6
|
+
|
|
7
|
+
/** Error code of a document whose required faces are still loading. */
|
|
8
|
+
export const FONTS_PENDING = "fonts-pending";
|
|
9
|
+
/** Error code of a document whose required faces could not be loaded. */
|
|
10
|
+
export const FONTS_UNAVAILABLE = "fonts-unavailable";
|
|
11
|
+
|
|
12
|
+
// A load that cannot finish (a face the registry never reports as loaded) stops after this many rounds.
|
|
13
|
+
const MAX_ROUNDS = 4;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Wrap a browser font registry (`loadBrowserFontRegistry` from `@openpresentation/opf-render/fonts-browser`).
|
|
17
|
+
* Registries without the lazy loaders (renderer 0.10.0 and older) gate nothing and keep the fonts they have.
|
|
18
|
+
*
|
|
19
|
+
* - `pending(document)` lists the faces (script packages and vendored files) the document needs that are not loaded.
|
|
20
|
+
* Synchronous; empty means the document can render now. Never throws: a document the renderer rejects for other
|
|
21
|
+
* reasons reports nothing pending and fails with its own validation message when rendered.
|
|
22
|
+
* - `ensure(document, { signal })` loads vendored faces first (script fallback depends on them), then script faces,
|
|
23
|
+
* and repeats until nothing is pending. It makes a bounded number of rounds and rejects with a `fonts-unavailable`
|
|
24
|
+
* error (the registry's error is its `cause`); it never retries by itself, a later call is a new attempt.
|
|
25
|
+
* - `run(document, handlers)` is `whenFontsReady` on this gate.
|
|
26
|
+
*/
|
|
27
|
+
export function createFontGate(registry) {
|
|
28
|
+
const pending = (document) => {
|
|
29
|
+
try {
|
|
30
|
+
const scripts = registry?.pendingScripts?.(document) ?? [];
|
|
31
|
+
const lazy = (registry?.pendingLazyFonts?.(document) ?? []).map((face) => face?.file ?? String(face));
|
|
32
|
+
return [...lazy, ...scripts];
|
|
33
|
+
} catch {
|
|
34
|
+
return [];
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
const gate = {
|
|
38
|
+
pending,
|
|
39
|
+
async ensure(document, options = {}) {
|
|
40
|
+
const { signal } = options;
|
|
41
|
+
try {
|
|
42
|
+
for (let round = 0; round < MAX_ROUNDS && pending(document).length; round++) {
|
|
43
|
+
signal?.throwIfAborted?.();
|
|
44
|
+
await registry.ensureLazyFonts?.(document, { signal });
|
|
45
|
+
await registry.ensureScripts?.(document, { signal });
|
|
46
|
+
}
|
|
47
|
+
} catch (cause) {
|
|
48
|
+
if (signal?.aborted) throw cause;
|
|
49
|
+
throw fontsUnavailable(cause?.message ?? String(cause), cause);
|
|
50
|
+
}
|
|
51
|
+
const left = pending(document);
|
|
52
|
+
if (left.length) throw fontsUnavailable(`${left.join(", ")} did not finish loading.`);
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
gate.run = (document, handlers) => whenFontsReady(gate, document, handlers);
|
|
56
|
+
return gate;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function fontsUnavailable(message, cause) {
|
|
60
|
+
const error = new Error(`Fonts for this document could not be loaded: ${message}`, cause ? { cause } : undefined);
|
|
61
|
+
error.code = FONTS_UNAVAILABLE;
|
|
62
|
+
return error;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** The error a synchronous render raises when it is asked to draw a document whose faces are pending. */
|
|
66
|
+
export function fontsPendingError(pending) {
|
|
67
|
+
const error = new Error("Loading fonts for this document…");
|
|
68
|
+
error.code = FONTS_PENDING;
|
|
69
|
+
error.pending = pending;
|
|
70
|
+
return error;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Ensure fonts for a document, then run `ready`: the one "ensure fonts, then render" helper.
|
|
75
|
+
* With nothing pending, `ready` runs synchronously, so documents in already-loaded fonts render at once. Otherwise
|
|
76
|
+
* `loading(pending)` runs, the faces load and `ready` runs after them. `isCurrent()` is checked after the wait, so a
|
|
77
|
+
* result for a superseded document is dropped. A load failure, and an exception thrown by `ready`, go to `failed(error)`
|
|
78
|
+
* (they are rethrown when there is no `failed`). `gate` is any object with `pending(document)` and `ensure(document)`;
|
|
79
|
+
* without one the document is treated as ready. Resolves once `ready` or `failed` has run.
|
|
80
|
+
*/
|
|
81
|
+
export function whenFontsReady(gate, document, { isCurrent = () => true, loading, ready, failed } = {}) {
|
|
82
|
+
const finish = () => {
|
|
83
|
+
try {
|
|
84
|
+
ready?.();
|
|
85
|
+
} catch (error) {
|
|
86
|
+
if (!failed) throw error;
|
|
87
|
+
failed(error);
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
const left = gate?.pending?.(document) ?? [];
|
|
91
|
+
if (!left.length) {
|
|
92
|
+
finish();
|
|
93
|
+
return Promise.resolve();
|
|
94
|
+
}
|
|
95
|
+
loading?.(left);
|
|
96
|
+
return Promise.resolve(gate.ensure(document)).then(
|
|
97
|
+
() => {
|
|
98
|
+
if (isCurrent()) finish();
|
|
99
|
+
},
|
|
100
|
+
(error) => {
|
|
101
|
+
if (isCurrent()) {
|
|
102
|
+
if (!failed) throw error;
|
|
103
|
+
failed(error);
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
);
|
|
107
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openpresentation/opf-editor",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.1",
|
|
4
4
|
"description": "Embeddable local OPF editor primitives built on the deterministic renderer.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -87,15 +87,15 @@
|
|
|
87
87
|
"sideEffects": false,
|
|
88
88
|
"scripts": {
|
|
89
89
|
"build": "node scripts/build.mjs",
|
|
90
|
-
"typecheck": "node --check src/index.js && node --check src/react.js && node --check src/svelte.js && node --check scripts/build.mjs && node --check scripts/validate-package.mjs && node --check test/smoke.mjs && node --check test/component-smoke.mjs && node --check src/canvas.js && node --check src/canvas-fields.js && node --check test/canvas-fields.mjs && node --check test/browser-canvas.mjs && node --check src/transfer.js && node --check src/presentation-ids.js && node --check src/font-defaults.js && node --check src/color-authoring.js && node --check test/presentation-ids.mjs && node --check src/galleries.js && node --check examples/transfer-controls.js && node --check src/schema.js && node --check src/schema-inspector.js && node --check test/rich-table-browser.mjs && node --check test/styled-table.mjs && node --check test/styled-table-browser.mjs && node --check examples/pptx-controls.js && node --check scripts/build-playground.mjs && node --check test/playground-pptx.mjs && node --check test/playground-script-fonts.mjs && node --check test/playground-lazy-fonts.mjs && node --check src/text-input.js && node --check src/rich-text.js && node --check src/rich-text-input.js && node --check src/rich-text-toolbar.js && node --check test/rich-text.mjs && node --check test/rich-input-browser.mjs && node --check src/json-editor.js && node --check src/json-menu.js && node --check src/json-options.js && node --check src/json-source.js && node --check src/switches.js && node --check test/switches.mjs && node --check test/switches-export.mjs && node --check test/placeholder-selection.mjs && node --check test/selection-browser.mjs && node --check scripts/check-font-hotlinks.mjs && node --check scripts/check-font-hotlinks.test.mjs && node --check src/exact-source.js && node --check test/exact-source.mjs && node --check test/playground-source.mjs",
|
|
91
|
-
"test": "npm run build && npm run check:font-hotlinks && node test/accepted-text.mjs && node test/smoke.mjs && node test/component-smoke.mjs && node test/canvas-fields.mjs && node test/transfer.mjs && node test/gallery-font-roles.mjs && node test/default-font-scheme.mjs && node test/presentation-ids.mjs && node test/schema.mjs && node test/rich-text.mjs && node test/layout.mjs && node test/blocks.mjs && node test/styled-table.mjs && node test/json-source.mjs && node test/json-options.mjs && node test/switches.mjs && node test/switches-export.mjs && node test/placeholder-selection.mjs && node test/exact-source.mjs",
|
|
90
|
+
"typecheck": "node --check src/index.js && node --check src/react.js && node --check src/svelte.js && node --check scripts/build.mjs && node --check scripts/validate-package.mjs && node --check test/smoke.mjs && node --check test/component-smoke.mjs && node --check src/canvas.js && node --check src/canvas-fields.js && node --check test/canvas-fields.mjs && node --check test/browser-canvas.mjs && node --check src/transfer.js && node --check src/presentation-ids.js && node --check src/font-defaults.js && node --check src/color-authoring.js && node --check test/presentation-ids.mjs && node --check src/galleries.js && node --check examples/transfer-controls.js && node --check src/schema.js && node --check src/schema-inspector.js && node --check test/rich-table-browser.mjs && node --check test/styled-table.mjs && node --check test/styled-table-browser.mjs && node --check examples/pptx-controls.js && node --check scripts/build-playground.mjs && node --check test/playground-pptx.mjs && node --check test/playground-script-fonts.mjs && node --check test/playground-lazy-fonts.mjs && node --check src/text-input.js && node --check src/rich-text.js && node --check src/rich-text-input.js && node --check src/rich-text-toolbar.js && node --check test/rich-text.mjs && node --check test/rich-input-browser.mjs && node --check src/json-editor.js && node --check src/json-menu.js && node --check src/json-options.js && node --check src/json-source.js && node --check src/switches.js && node --check test/switches.mjs && node --check test/switches-export.mjs && node --check test/placeholder-selection.mjs && node --check test/selection-browser.mjs && node --check scripts/check-font-hotlinks.mjs && node --check scripts/check-font-hotlinks.test.mjs && node --check src/exact-source.js && node --check test/exact-source.mjs && node --check test/playground-source.mjs && node --check src/font-gate.js && node --check test/font-gate.mjs && node --check test/playground-ensure-fonts.mjs && node --check test/canvas-fonts-browser.mjs",
|
|
91
|
+
"test": "npm run build && npm run check:font-hotlinks && node test/accepted-text.mjs && node test/smoke.mjs && node test/component-smoke.mjs && node test/canvas-fields.mjs && node test/transfer.mjs && node test/gallery-font-roles.mjs && node test/default-font-scheme.mjs && node test/presentation-ids.mjs && node test/schema.mjs && node test/rich-text.mjs && node test/layout.mjs && node test/blocks.mjs && node test/styled-table.mjs && node test/json-source.mjs && node test/json-options.mjs && node test/switches.mjs && node test/switches-export.mjs && node test/placeholder-selection.mjs && node test/exact-source.mjs && node test/font-gate.mjs",
|
|
92
92
|
"validate": "node scripts/validate-package.mjs",
|
|
93
93
|
"test:packed": "node test/packed-install.mjs",
|
|
94
94
|
"prepack": "npm run build",
|
|
95
95
|
"check:font-hotlinks": "node --test scripts/check-font-hotlinks.test.mjs && node scripts/check-font-hotlinks.mjs",
|
|
96
96
|
"check:font-hotlinks:built": "node scripts/check-font-hotlinks.mjs --built dist artifacts/playground",
|
|
97
97
|
"build:playground": "node scripts/build-playground.mjs",
|
|
98
|
-
"test:playground": "npm run build:playground && npm run check:font-hotlinks:built && node test/playground-pptx.mjs && node test/playground-script-fonts.mjs artifacts/playground-script-fonts && node test/playground-lazy-fonts.mjs artifacts/playground-lazy-fonts && node test/playground-source.mjs",
|
|
98
|
+
"test:playground": "npm run build:playground && npm run check:font-hotlinks:built && node test/playground-pptx.mjs && node test/playground-script-fonts.mjs artifacts/playground-script-fonts && node test/playground-lazy-fonts.mjs artifacts/playground-lazy-fonts && node test/playground-source.mjs && node test/playground-ensure-fonts.mjs artifacts/playground-ensure-fonts && node test/canvas-fonts-browser.mjs artifacts/canvas-fonts",
|
|
99
99
|
"test:code-browser": "node test/code-browser.mjs",
|
|
100
100
|
"test:selection-browser": "node test/selection-browser.mjs",
|
|
101
101
|
"test:metric-browser": "node test/metric-browser.mjs",
|
|
@@ -156,8 +156,8 @@
|
|
|
156
156
|
"@expo-google-fonts/noto-sans-thai": "0.4.2",
|
|
157
157
|
"@expo-google-fonts/noto-serif-hebrew": "0.4.2",
|
|
158
158
|
"@expo-google-fonts/noto-serif-tibetan": "0.4.2",
|
|
159
|
-
"@openpresentation/opf-pptx": "^0.11.
|
|
160
|
-
"@openpresentation/opf-render": "^0.11.
|
|
159
|
+
"@openpresentation/opf-pptx": "^0.11.1",
|
|
160
|
+
"@openpresentation/opf-render": "^0.11.2",
|
|
161
161
|
"esbuild": "0.28.2",
|
|
162
162
|
"playwright": "1.63.0"
|
|
163
163
|
}
|