@openpresentation/opf-editor 0.11.3 → 0.14.2
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 +82 -58
- package/dist/annotations.d.ts +16 -16
- package/dist/annotations.js +71 -79
- package/dist/assets.d.ts +7 -7
- package/dist/assets.js +15 -15
- package/dist/background-options.d.ts +2 -2
- package/dist/background-options.js +6 -6
- package/dist/block-controls.js +7 -7
- package/dist/block-convert.d.ts +7 -7
- package/dist/block-convert.js +23 -21
- package/dist/blocks.d.ts +8 -8
- package/dist/blocks.js +41 -40
- package/dist/canvas-fields.js +35 -12
- package/dist/canvas.d.ts +22 -30
- package/dist/canvas.js +57 -64
- package/dist/chart-data.d.ts +12 -10
- package/dist/chart-data.js +22 -21
- package/dist/chart-options-panel.d.ts +2 -2
- package/dist/chart-options-panel.js +82 -8
- package/dist/chart-options.d.ts +35 -9
- package/dist/chart-options.js +133 -40
- package/dist/checks.js +21 -0
- package/dist/color-authoring.js +1 -1
- package/dist/content-actions.d.ts +15 -15
- package/dist/content-actions.js +71 -70
- package/dist/content-controls.js +12 -12
- package/dist/data-grid.js +179 -10
- package/dist/data.d.ts +23 -2
- package/dist/data.js +40 -1
- package/dist/dataset-refs.d.ts +17 -0
- package/dist/dataset-refs.js +45 -0
- package/dist/design-controls.js +66 -47
- package/dist/design-options.d.ts +16 -14
- package/dist/design-options.js +86 -74
- package/dist/edit-helpers.js +8 -7
- package/dist/exact-source.js +5 -5
- package/dist/export.d.ts +5 -7
- package/dist/export.js +40 -14
- package/dist/find-panel.js +5 -5
- package/dist/find-replace.d.ts +3 -3
- package/dist/find-replace.js +35 -20
- package/dist/font-gate.js +49 -58
- package/dist/galleries.js +36 -52
- package/dist/grid-model.d.ts +99 -24
- package/dist/grid-model.js +577 -101
- package/dist/image-crop.d.ts +4 -4
- package/dist/image-crop.js +20 -20
- package/dist/image-cropper.js +3 -3
- package/dist/index.d.ts +28 -28
- package/dist/index.js +74 -90
- package/dist/json-options.js +17 -16
- package/dist/layout-handles.js +5 -5
- package/dist/layout-placeholders.js +4 -4
- package/dist/layout.d.ts +2 -2
- package/dist/layout.js +8 -7
- package/dist/numbering-panel.js +3 -5
- package/dist/numbering.d.ts +3 -5
- package/dist/numbering.js +14 -26
- package/dist/outline-view.js +24 -24
- package/dist/outline.d.ts +12 -12
- package/dist/outline.js +52 -50
- package/dist/persistence.d.ts +1 -1
- package/dist/persistence.js +19 -18
- package/dist/presentation-ids.js +2 -2
- package/dist/react.js +3 -3
- package/dist/review-panel.d.ts +30 -13
- package/dist/review-panel.js +154 -53
- package/dist/review.d.ts +36 -43
- package/dist/review.js +92 -53
- package/dist/rich-text-input.js +4 -3
- package/dist/rich-text-toolbar.js +5 -3
- package/dist/rich-text.js +5 -2
- package/dist/schema-inspector.d.ts +2 -2
- package/dist/schema-inspector.js +16 -11
- package/dist/schema.d.ts +1 -1
- package/dist/schema.js +25 -5
- package/dist/slide-manager.d.ts +2 -2
- package/dist/slide-manager.js +18 -18
- package/dist/slides.d.ts +20 -20
- package/dist/slides.js +99 -97
- package/dist/svelte.js +1 -1
- package/dist/switches.d.ts +10 -10
- package/dist/switches.js +136 -109
- package/dist/table-options.d.ts +8 -8
- package/dist/table-options.js +41 -30
- package/dist/table-structure.d.ts +10 -10
- package/dist/table-structure.js +21 -21
- package/dist/template-panel.d.ts +3 -3
- package/dist/template-panel.js +37 -13
- package/dist/templates.d.ts +22 -11
- package/dist/templates.js +48 -55
- package/dist/transfer.d.ts +4 -4
- package/dist/transfer.js +74 -54
- package/package.json +10 -6
- package/dist/font-defaults.js +0 -25
package/dist/find-panel.js
CHANGED
|
@@ -177,7 +177,7 @@ export function createFindPanel(container, options) {
|
|
|
177
177
|
|
|
178
178
|
function search(keep) {
|
|
179
179
|
const before = current >= 0 ? result.matches[current] : undefined;
|
|
180
|
-
result = findMatches(editor.
|
|
180
|
+
result = findMatches(editor.presentation, state.query, sessionOptions());
|
|
181
181
|
setError(result.error);
|
|
182
182
|
if (!result.matches.length) current = -1;
|
|
183
183
|
else if (keep && before) {
|
|
@@ -231,8 +231,8 @@ export function createFindPanel(container, options) {
|
|
|
231
231
|
function doReplaceOne() {
|
|
232
232
|
const match = result.matches[current];
|
|
233
233
|
if (!match || !finishCommit()) return;
|
|
234
|
-
const order = new Map(collectSearchFields(editor.
|
|
235
|
-
const fieldText = textOf(getValueAtPath(editor.
|
|
234
|
+
const order = new Map(collectSearchFields(editor.presentation, sessionOptions()).map((field, index) => [field.pointer, index]));
|
|
235
|
+
const fieldText = textOf(getValueAtPath(editor.presentation, match.pointer));
|
|
236
236
|
const written = match.start + expandReplacement(state.replacement, match, fieldText, state.regex).length;
|
|
237
237
|
try {
|
|
238
238
|
replaceMatch(editor, match, state.query, state.replacement, sessionOptions());
|
|
@@ -246,8 +246,8 @@ export function createFindPanel(container, options) {
|
|
|
246
246
|
setError("");
|
|
247
247
|
// Move on to the next match after the replaced text, in reading order (never the text just written).
|
|
248
248
|
const replacedAt = order.get(match.pointer) ?? -1;
|
|
249
|
-
result = findMatches(editor.
|
|
250
|
-
const nextOrder = new Map(collectSearchFields(editor.
|
|
249
|
+
result = findMatches(editor.presentation, state.query, sessionOptions());
|
|
250
|
+
const nextOrder = new Map(collectSearchFields(editor.presentation, sessionOptions()).map((field, index) => [field.pointer, index]));
|
|
251
251
|
let next = result.matches.findIndex((candidate) => {
|
|
252
252
|
const position = nextOrder.get(candidate.pointer) ?? Infinity;
|
|
253
253
|
return position > replacedAt || (position === replacedAt && candidate.start >= written);
|
package/dist/find-replace.d.ts
CHANGED
|
@@ -80,11 +80,11 @@ export interface TextEdit {
|
|
|
80
80
|
/** The plain text of a string or a run array. */
|
|
81
81
|
export declare function textOf(value: unknown): string;
|
|
82
82
|
/** Every searchable text field of a document, in reading order. */
|
|
83
|
-
export declare function collectSearchFields(
|
|
83
|
+
export declare function collectSearchFields(presentation: unknown, options?: CollectOptions): SearchField[];
|
|
84
84
|
/** Compile a search into a global RegExp; `{ error }` for an invalid regular expression, `{ empty: true }` for an empty query. */
|
|
85
85
|
export declare function compileSearch(query: string, options?: SearchOptions): { regex?: RegExp; error?: string; empty?: boolean };
|
|
86
86
|
/** Search a document's text. */
|
|
87
|
-
export declare function findMatches(
|
|
87
|
+
export declare function findMatches(presentation: unknown, query: string, options?: SearchOptions): SearchResult;
|
|
88
88
|
/** Expand a replacement for one match (`$&`, `$1`, `$<name>`, `$$` in regex mode; literal otherwise). */
|
|
89
89
|
export declare function expandReplacement(template: string, match: SearchMatch, fieldText: string, regex?: boolean): string;
|
|
90
90
|
/**
|
|
@@ -93,7 +93,7 @@ export declare function expandReplacement(template: string, match: SearchMatch,
|
|
|
93
93
|
*/
|
|
94
94
|
export declare function applyTextEdits<T extends string | unknown[]>(value: T, edits: TextEdit[]): T;
|
|
95
95
|
/** The patches that replace the given matches. Throws `stale-match` when the text changed. */
|
|
96
|
-
export declare function planReplace(
|
|
96
|
+
export declare function planReplace(presentation: unknown, matches: SearchMatch[], replacement: string, options?: { regex?: boolean }): ReplacePlan;
|
|
97
97
|
/** Replace every match as one undoable session change. Throws `invalid-search` for a bad regular expression. */
|
|
98
98
|
export declare function replaceAll(editor: EditorSession, query: string, replacement: string, options?: SearchOptions): ReplaceResult;
|
|
99
99
|
/** Replace one match as one undoable change. Throws `stale-match` when it is no longer in the document. */
|
package/dist/find-replace.js
CHANGED
|
@@ -81,6 +81,8 @@ function payloadFields(fields, state, base, payload, prefix = "") {
|
|
|
81
81
|
else if (isObject(payload.quote)) {
|
|
82
82
|
field(fields, state, at("quote", "text"), payload.quote.text, `${prefix}Quote`);
|
|
83
83
|
field(fields, state, at("quote", "attribution"), payload.quote.attribution, `${prefix}Quote attribution`);
|
|
84
|
+
field(fields, state, at("quote", "role"), payload.quote.role, `${prefix}Quote role`);
|
|
85
|
+
if (isObject(payload.quote.photo)) field(fields, state, at("quote", "photo", "alt"), payload.quote.photo.alt, `${prefix}Quote photo alt text`, "alt");
|
|
84
86
|
field(fields, state, at("quote", "source"), payload.quote.source, `${prefix}Quote source`);
|
|
85
87
|
}
|
|
86
88
|
const timeline = payload.timeline;
|
|
@@ -97,14 +99,17 @@ function payloadFields(fields, state, base, payload, prefix = "") {
|
|
|
97
99
|
const chart = payload.chart;
|
|
98
100
|
if (isObject(chart) && isObject(chart.data)) {
|
|
99
101
|
const { columns, rows } = chart.data;
|
|
100
|
-
|
|
102
|
+
// A column name that `mapping` addresses is not replaced: the mapping would stop finding it.
|
|
103
|
+
if (Array.isArray(columns) && !isObject(chart.mapping))
|
|
104
|
+
columns.forEach((value, index) => (isObject(value) ? field(fields, state, at("chart", "data", "columns", String(index), "name"), value.name, `${prefix}Chart label ${index + 1}`, "chart") : field(fields, state, at("chart", "data", "columns", String(index)), value, `${prefix}Chart label ${index + 1}`, "chart")));
|
|
101
105
|
if (Array.isArray(rows)) rows.forEach((row, r) => Array.isArray(row) && row.forEach((value, c) => {
|
|
102
106
|
if (typeof value === "string") field(fields, state, at("chart", "data", "rows", String(r), String(c)), value, `${prefix}Chart row ${r + 1}, column ${c + 1}`, "chart");
|
|
103
107
|
}));
|
|
104
108
|
}
|
|
109
|
+
if (isObject(payload.chart)) field(fields, state, at("chart", "alt"), payload.chart.alt, `${prefix}Chart alt text`, "alt");
|
|
105
110
|
const table = payload.table;
|
|
106
111
|
if (isObject(table)) {
|
|
107
|
-
if (Array.isArray(table.columns)) table.columns.forEach((cell, c) => cellFields(fields, state, at("table", "columns", String(c)), cell, `${prefix}Table header ${c + 1}`));
|
|
112
|
+
if (Array.isArray(table.columns)) table.columns.forEach((cell, c) => (isObject(cell) && !("value" in cell) ? field(fields, state, at("table", "columns", String(c), "name"), cell.name, `${prefix}Table header ${c + 1}`, "table") : cellFields(fields, state, at("table", "columns", String(c)), cell, `${prefix}Table header ${c + 1}`)));
|
|
108
113
|
if (Array.isArray(table.rows)) table.rows.forEach((row, r) => Array.isArray(row) && row.forEach((cell, c) => cellFields(fields, state, at("table", "rows", String(r), String(c)), cell, `${prefix}Table row ${r + 1}, column ${c + 1}`)));
|
|
109
114
|
}
|
|
110
115
|
if (isObject(payload.image)) field(fields, state, at("image", "alt"), payload.image.alt, `${prefix}Image alt text`, "alt");
|
|
@@ -129,17 +134,27 @@ function furnitureFields(fields, state, base, design) {
|
|
|
129
134
|
* the deck, `pointer` is the JSON pointer of the string or run array, and `runs` says it is a run array. Options:
|
|
130
135
|
* `notes: false` leaves speaker notes out, `slideIndex` limits the list to one slide.
|
|
131
136
|
*/
|
|
132
|
-
export function collectSearchFields(
|
|
137
|
+
export function collectSearchFields(presentation, options = {}) {
|
|
133
138
|
const fields = [];
|
|
134
|
-
if (!isObject(
|
|
139
|
+
if (!isObject(presentation)) return fields;
|
|
135
140
|
const only = Number.isInteger(options.slideIndex) ? options.slideIndex : undefined;
|
|
136
141
|
if (only === undefined) {
|
|
137
142
|
const deck = { slideIndex: -1 };
|
|
138
|
-
field(fields, deck, ["name"],
|
|
139
|
-
field(fields, deck, ["description"],
|
|
140
|
-
furnitureFields(fields, deck, [],
|
|
143
|
+
field(fields, deck, ["name"], presentation.name, "Presentation name", "deck");
|
|
144
|
+
field(fields, deck, ["description"], presentation.description, "Presentation description", "deck");
|
|
145
|
+
furnitureFields(fields, deck, [], presentation.design);
|
|
141
146
|
}
|
|
142
|
-
|
|
147
|
+
// RR-54: the text cells of a shared dataset belong to the deck. Its column names are left alone (`fields` and `mapping` address them).
|
|
148
|
+
if (only === undefined && isObject(presentation.datasets)) {
|
|
149
|
+
const deck = { slideIndex: -1 };
|
|
150
|
+
for (const [id, dataset] of Object.entries(presentation.datasets)) {
|
|
151
|
+
if (!isObject(dataset) || !Array.isArray(dataset.rows)) continue;
|
|
152
|
+
dataset.rows.forEach((row, r) => Array.isArray(row) && row.forEach((value, c) => {
|
|
153
|
+
if (typeof value === "string") field(fields, deck, ["datasets", id, "rows", String(r), String(c)], value, `Dataset ${id}, row ${r + 1}, column ${c + 1}`, "dataset");
|
|
154
|
+
}));
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
(Array.isArray(presentation.slides) ? presentation.slides : []).forEach((slide, slideIndex) => {
|
|
143
158
|
if (only !== undefined && slideIndex !== only) return;
|
|
144
159
|
if (!isObject(slide)) return;
|
|
145
160
|
const state = { slideIndex };
|
|
@@ -191,12 +206,12 @@ function snippet(text, start, end) {
|
|
|
191
206
|
* are offsets into the field's text. Options: those of `compileSearch`, plus `notes` and `slideIndex` of
|
|
192
207
|
* `collectSearchFields`. Matches that are empty (a regular expression that matches nothing) are skipped.
|
|
193
208
|
*/
|
|
194
|
-
export function findMatches(
|
|
209
|
+
export function findMatches(presentation, query, options = {}) {
|
|
195
210
|
const compiled = compileSearch(query, options);
|
|
196
211
|
const result = { matches: [], fieldCount: 0, slideCount: 0, truncated: false, error: compiled.error };
|
|
197
212
|
if (!compiled.regex) return result;
|
|
198
213
|
const slides = new Set();
|
|
199
|
-
const fields = collectSearchFields(
|
|
214
|
+
const fields = collectSearchFields(presentation, options);
|
|
200
215
|
outer: for (const item of fields) {
|
|
201
216
|
compiled.regex.lastIndex = 0;
|
|
202
217
|
let found;
|
|
@@ -308,8 +323,8 @@ export function applyTextEdits(value, edits) {
|
|
|
308
323
|
return result;
|
|
309
324
|
}
|
|
310
325
|
|
|
311
|
-
function readField(
|
|
312
|
-
const value = getValueAtPath(
|
|
326
|
+
function readField(presentation, match) {
|
|
327
|
+
const value = getValueAtPath(presentation, match.pointer);
|
|
313
328
|
return typeof value === "string" || Array.isArray(value) ? value : undefined;
|
|
314
329
|
}
|
|
315
330
|
|
|
@@ -318,7 +333,7 @@ function readField(document, match) {
|
|
|
318
333
|
* `{ patches, count, fieldCount }`; a replacement that leaves a field unchanged adds no patch. Throws `stale-match`
|
|
319
334
|
* when a match no longer reads the same text in the document.
|
|
320
335
|
*/
|
|
321
|
-
export function planReplace(
|
|
336
|
+
export function planReplace(presentation, matches, replacement, options = {}) {
|
|
322
337
|
const byField = new Map();
|
|
323
338
|
for (const match of matches) {
|
|
324
339
|
if (!byField.has(match.pointer)) byField.set(match.pointer, []);
|
|
@@ -327,7 +342,7 @@ export function planReplace(document, matches, replacement, options = {}) {
|
|
|
327
342
|
const patches = [];
|
|
328
343
|
let count = 0;
|
|
329
344
|
for (const [pointer, list] of byField) {
|
|
330
|
-
const value = readField(
|
|
345
|
+
const value = readField(presentation, list[0]);
|
|
331
346
|
const text = value === undefined ? undefined : textOf(value);
|
|
332
347
|
const ordered = [...list].sort((a, b) => a.start - b.start);
|
|
333
348
|
if (text === undefined || ordered.some((match) => text.slice(match.start, match.end) !== match.text)) {
|
|
@@ -341,8 +356,8 @@ export function planReplace(document, matches, replacement, options = {}) {
|
|
|
341
356
|
return { patches, count, fieldCount: byField.size };
|
|
342
357
|
}
|
|
343
358
|
|
|
344
|
-
function search(
|
|
345
|
-
const found = findMatches(
|
|
359
|
+
function search(presentation, query, options) {
|
|
360
|
+
const found = findMatches(presentation, query, options);
|
|
346
361
|
if (found.error) throw new OPFEditorError("invalid-search", found.error, {});
|
|
347
362
|
return found;
|
|
348
363
|
}
|
|
@@ -352,8 +367,8 @@ function search(document, query, options) {
|
|
|
352
367
|
* the session's result (or null when nothing changed). One undo restores every replaced field.
|
|
353
368
|
*/
|
|
354
369
|
export function replaceAll(editor, query, replacement, options = {}) {
|
|
355
|
-
const found = search(editor.
|
|
356
|
-
const plan = planReplace(editor.
|
|
370
|
+
const found = search(editor.presentation, query, options);
|
|
371
|
+
const plan = planReplace(editor.presentation, found.matches, String(replacement ?? ""), options);
|
|
357
372
|
if (!plan.patches.length) return { count: 0, fieldCount: 0, change: null };
|
|
358
373
|
const change = editor.applyPatch(plan.patches, { source: "find-replace", rejectInvalid: true, replaced: plan.count });
|
|
359
374
|
return { count: plan.count, fieldCount: plan.fieldCount, change };
|
|
@@ -364,10 +379,10 @@ export function replaceAll(editor, query, replacement, options = {}) {
|
|
|
364
379
|
* current document first: `stale-match` is thrown when the text changed underneath it.
|
|
365
380
|
*/
|
|
366
381
|
export function replaceMatch(editor, match, query, replacement, options = {}) {
|
|
367
|
-
const found = search(editor.
|
|
382
|
+
const found = search(editor.presentation, query, options);
|
|
368
383
|
const current = found.matches.find((candidate) => candidate.pointer === match.pointer && candidate.start === match.start && candidate.end === match.end && candidate.text === match.text);
|
|
369
384
|
if (!current) throw new OPFEditorError("stale-match", "That match is no longer in the document. Search again.", { path: match.path });
|
|
370
|
-
const plan = planReplace(editor.
|
|
385
|
+
const plan = planReplace(editor.presentation, [current], String(replacement ?? ""), options);
|
|
371
386
|
if (!plan.patches.length) return { count: 0, change: null };
|
|
372
387
|
const change = editor.applyPatch(plan.patches, { source: "find-replace", rejectInvalid: true, replaced: 1 });
|
|
373
388
|
return { count: 1, change };
|
package/dist/font-gate.js
CHANGED
|
@@ -1,75 +1,65 @@
|
|
|
1
|
-
// Fonts before pixels. A document can need faces the browser
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
1
|
+
// Fonts before pixels. A document can need faces the browser has not loaded yet: script faces for the languages it draws
|
|
2
|
+
// (Japanese, Arabic, ...) and vendored preview faces for the font families it resolves (Intos for Aptos, Open Sans, Barlow,
|
|
3
|
+
// ...). The renderer cannot draw such a document until those faces are loaded (a deck that sets Aptos and contains Japanese
|
|
4
|
+
// fails with "Intos Display cannot display U+65E5"), so nothing may render or measure a new document before `ensure`
|
|
5
|
+
// resolves. The renderer's fonts handle (`loadFonts()` from `@openpresentation/opf-render/fonts-browser`) is the source of
|
|
6
|
+
// truth: its `pending(presentation, renderOptions)` and `ensure(presentation, options)` know what is missing and load it. The
|
|
7
|
+
// canvas and `exportDeck` take that handle as `fonts` and gate every document-replacing path through this module.
|
|
6
8
|
|
|
7
9
|
/** Error code of a document whose required faces are still loading. */
|
|
8
10
|
export const FONTS_PENDING = "fonts-pending";
|
|
9
11
|
/** Error code of a document whose required faces could not be loaded. */
|
|
10
12
|
export const FONTS_UNAVAILABLE = "fonts-unavailable";
|
|
11
13
|
|
|
12
|
-
|
|
13
|
-
const MAX_ROUNDS = 4;
|
|
14
|
+
const GATE = Symbol("opf-editor.fontGate");
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
|
-
*
|
|
17
|
-
*
|
|
17
|
+
* The fonts a preview draws with: the handle's text measurement and nothing else. A full handle also lists `embeddedFonts`, which
|
|
18
|
+
* the renderer writes into every SVG it returns (an export needs that, so the SVG carries its faces); a preview inside a page
|
|
19
|
+
* that already loaded the faces would repeat megabytes of base64 in every canvas, thumbnail and panel preview. The measurement
|
|
20
|
+
* is read from the handle at each use, so faces it loads later still measure.
|
|
21
|
+
*/
|
|
22
|
+
export function previewFonts(fonts) {
|
|
23
|
+
if (!fonts || typeof fonts !== "object") return undefined;
|
|
24
|
+
return { get textMeasurement() { return fonts.textMeasurement; } };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The gate over a fonts handle: the two calls the canvas and the export need, with the editor's error contract.
|
|
29
|
+
*
|
|
30
|
+
* - `pending(presentation, renderOptions?)` lists the faces (vendored files and script packages) the document needs that are
|
|
31
|
+
* not loaded. Synchronous; empty means the document can render now. Never throws: a document the renderer rejects for other
|
|
32
|
+
* reasons reports nothing pending and fails with its own validation message when rendered. A handle without `pending`
|
|
33
|
+
* (a plain `{ textMeasurement }`) gates nothing.
|
|
34
|
+
* - `ensure(presentation, { signal, renderOptions })` loads what is missing through the handle's `ensure`, then checks that
|
|
35
|
+
* nothing is left. It rejects with a `fonts-unavailable` error (the handle's error is its `cause`), never retries by itself,
|
|
36
|
+
* and rethrows an abort as it is.
|
|
18
37
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* other reasons reports nothing pending and fails with its own validation message when rendered. Script faces and
|
|
22
|
-
* vendored faces are asked separately, so one that cannot be answered does not hide the other.
|
|
23
|
-
* - `ensure(document, { signal, renderOptions })` loads vendored faces first (script fallback depends on them), then script faces,
|
|
24
|
-
* and repeats until nothing is pending. It makes a bounded number of rounds and rejects with a `fonts-unavailable`
|
|
25
|
-
* error (the registry's error is its `cause`); it never retries by itself, a later call is a new attempt.
|
|
26
|
-
* - `run(document, handlers)` is `whenFontsReady` on this gate.
|
|
27
|
-
* - `renderOptions` (per call, or `createFontGate(registry, { renderOptions })` as an object or a getter) are the options the
|
|
28
|
-
* host renders with (`catalogs`, ...). They are passed to the registry, so a layout or font scheme that only the host's
|
|
29
|
-
* catalogs know resolves the same way there (renderer 0.11.5 and newer; older renderers ignore them).
|
|
38
|
+
* `renderOptions` are the options the host renders with (`catalogs`, ...); they reach the handle, so a layout or font scheme
|
|
39
|
+
* that only the host's catalogs know resolves the same way there. Hand a gate back unchanged.
|
|
30
40
|
*/
|
|
31
|
-
export function
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const optionsFor = (renderOptions) => ({ ...defaults(), ...renderOptions });
|
|
37
|
-
// Each source answers on its own: a document one loader cannot resolve (an unresolvable layout throws in the renderer's
|
|
38
|
-
// lazy font loader) must not hide what the other still reports pending.
|
|
39
|
-
const sources = (document, renderOptions) => {
|
|
40
|
-
const options = optionsFor(renderOptions);
|
|
41
|
-
let scripts = [], lazy = [];
|
|
42
|
-
try { scripts = registry?.pendingScripts?.(document, options) ?? []; } catch { /* the renderer reports the document itself when it renders */ }
|
|
43
|
-
try { lazy = (registry?.pendingLazyFonts?.(document, options) ?? []).map((face) => face?.file ?? String(face)); } catch { /* likewise */ }
|
|
44
|
-
return { lazy, scripts };
|
|
45
|
-
};
|
|
46
|
-
const pending = (document, renderOptions) => {
|
|
47
|
-
const { lazy, scripts } = sources(document, renderOptions);
|
|
48
|
-
return [...lazy, ...scripts];
|
|
41
|
+
export function fontGate(fonts) {
|
|
42
|
+
if (!fonts || typeof fonts !== "object") return undefined;
|
|
43
|
+
if (fonts[GATE]) return fonts;
|
|
44
|
+
const pending = (presentation, renderOptions) => {
|
|
45
|
+
try { return fonts.pending?.(presentation, renderOptions) ?? []; } catch { return []; /* the renderer reports the document itself when it renders */ }
|
|
49
46
|
};
|
|
50
47
|
const gate = {
|
|
48
|
+
[GATE]: true,
|
|
51
49
|
pending,
|
|
52
|
-
async ensure(
|
|
53
|
-
|
|
54
|
-
const call = { ...optionsFor(renderOptions), signal };
|
|
50
|
+
async ensure(presentation, { signal, renderOptions } = {}) {
|
|
51
|
+
if (!pending(presentation, renderOptions).length) return;
|
|
55
52
|
try {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
if (!lazy.length && !scripts.length) break;
|
|
59
|
-
signal?.throwIfAborted?.();
|
|
60
|
-
// Vendored faces first (script fallback depends on them). A source with nothing pending is not asked to load.
|
|
61
|
-
if (lazy.length) await registry.ensureLazyFonts?.(document, call);
|
|
62
|
-
if (scripts.length) await registry.ensureScripts?.(document, call);
|
|
63
|
-
}
|
|
53
|
+
signal?.throwIfAborted?.();
|
|
54
|
+
await fonts.ensure(presentation, { ...renderOptions, signal });
|
|
64
55
|
} catch (cause) {
|
|
65
56
|
if (signal?.aborted) throw cause;
|
|
66
57
|
throw fontsUnavailable(cause?.message ?? String(cause), cause);
|
|
67
58
|
}
|
|
68
|
-
const left = pending(
|
|
59
|
+
const left = pending(presentation, renderOptions);
|
|
69
60
|
if (left.length) throw fontsUnavailable(`${left.join(", ")} did not finish loading.`);
|
|
70
61
|
},
|
|
71
62
|
};
|
|
72
|
-
gate.run = (document, handlers) => whenFontsReady(gate, document, handlers);
|
|
73
63
|
return gate;
|
|
74
64
|
}
|
|
75
65
|
|
|
@@ -88,15 +78,16 @@ export function fontsPendingError(pending) {
|
|
|
88
78
|
}
|
|
89
79
|
|
|
90
80
|
/**
|
|
91
|
-
* Ensure fonts for a
|
|
81
|
+
* Ensure fonts for a presentation, then run `ready`: the one "ensure fonts, then render" helper.
|
|
92
82
|
* With nothing pending, `ready` runs synchronously, so documents in already-loaded fonts render at once. Otherwise
|
|
93
83
|
* `loading(pending)` runs, the faces load and `ready` runs after them. `isCurrent()` is checked after the wait, so a
|
|
94
84
|
* result for a superseded document is dropped. A load failure, and an exception thrown by `ready`, go to `failed(error)`
|
|
95
|
-
* (they are rethrown when there is no `failed`). `renderOptions`
|
|
96
|
-
* `pending(
|
|
97
|
-
*
|
|
85
|
+
* (they are rethrown when there is no `failed`). `renderOptions` reach the fonts handle. `fonts` is the renderer's fonts
|
|
86
|
+
* handle (anything with `pending(presentation, renderOptions)` and `ensure(presentation, options)`); without one the
|
|
87
|
+
* document is treated as ready. Resolves once `ready` or `failed` has run.
|
|
98
88
|
*/
|
|
99
|
-
export function whenFontsReady(
|
|
89
|
+
export function whenFontsReady(fonts, presentation, { isCurrent = () => true, loading, ready, failed, renderOptions } = {}) {
|
|
90
|
+
const gate = fontGate(fonts);
|
|
100
91
|
const finish = () => {
|
|
101
92
|
try {
|
|
102
93
|
ready?.();
|
|
@@ -105,13 +96,13 @@ export function whenFontsReady(gate, document, { isCurrent = () => true, loading
|
|
|
105
96
|
failed(error);
|
|
106
97
|
}
|
|
107
98
|
};
|
|
108
|
-
const left = gate?.pending
|
|
99
|
+
const left = gate?.pending(presentation, renderOptions) ?? [];
|
|
109
100
|
if (!left.length) {
|
|
110
101
|
finish();
|
|
111
102
|
return Promise.resolve();
|
|
112
103
|
}
|
|
113
104
|
loading?.(left);
|
|
114
|
-
return Promise.resolve(gate.ensure(
|
|
105
|
+
return Promise.resolve(gate.ensure(presentation, { renderOptions })).then(
|
|
115
106
|
() => {
|
|
116
107
|
if (isCurrent()) finish();
|
|
117
108
|
},
|
package/dist/galleries.js
CHANGED
|
@@ -119,28 +119,15 @@ export async function loadOpfGallery(input, options = {}) {
|
|
|
119
119
|
}
|
|
120
120
|
const FONT_SCHEME_ENUMS = {
|
|
121
121
|
type: ["sans-serif", "serif", "monospace"],
|
|
122
|
-
app: ["
|
|
123
|
-
languageFamily: ["latin", "ea", "cs"],
|
|
122
|
+
app: ["powerpoint", "google-slides"],
|
|
123
|
+
languageFamily: ["latin", "ea", "cs", "eastAsian", "complexScript"],
|
|
124
124
|
};
|
|
125
|
-
// A
|
|
125
|
+
// A font role is a family name (a non-empty string), or undefined.
|
|
126
126
|
function fontRole(value) {
|
|
127
|
-
|
|
128
|
-
if (!value || typeof value !== "object" || typeof value.family !== "string")
|
|
129
|
-
return undefined;
|
|
130
|
-
const role = { family: value.family };
|
|
131
|
-
if (typeof value.weight === "number" && Number.isFinite(value.weight))
|
|
132
|
-
role.weight = value.weight;
|
|
133
|
-
if (value.style === "normal" || value.style === "italic")
|
|
134
|
-
role.style = value.style;
|
|
135
|
-
if (
|
|
136
|
-
typeof value.letterSpacing === "number" &&
|
|
137
|
-
Number.isFinite(value.letterSpacing)
|
|
138
|
-
)
|
|
139
|
-
role.letterSpacing = value.letterSpacing;
|
|
140
|
-
return role;
|
|
127
|
+
return typeof value === "string" && value ? value : undefined;
|
|
141
128
|
}
|
|
142
129
|
// Keep every font-scheme role that the catalog record schema defines: the OOXML
|
|
143
|
-
// pair and `code`. Gallery `heading`/`body` (
|
|
130
|
+
// pair and `code`. Gallery `heading`/`body` (family names) map onto
|
|
144
131
|
// major/minor only, never role objects, so a later inline design.fontScheme
|
|
145
132
|
// major/minor override still wins (resolveFontFamilies checks roles before the
|
|
146
133
|
// pair). `accent` is not part of the record schema and is dropped.
|
|
@@ -152,11 +139,11 @@ function fontSchemeRecord(source, id) {
|
|
|
152
139
|
major:
|
|
153
140
|
typeof source.major === "string"
|
|
154
141
|
? source.major
|
|
155
|
-
: fontRole(source.heading)
|
|
142
|
+
: fontRole(source.heading),
|
|
156
143
|
minor:
|
|
157
144
|
typeof source.minor === "string"
|
|
158
145
|
? source.minor
|
|
159
|
-
: fontRole(source.body)
|
|
146
|
+
: fontRole(source.body),
|
|
160
147
|
};
|
|
161
148
|
for (const [field, values] of Object.entries(FONT_SCHEME_ENUMS))
|
|
162
149
|
if (values.includes(source[field])) record[field] = source[field];
|
|
@@ -164,7 +151,7 @@ function fontSchemeRecord(source, id) {
|
|
|
164
151
|
if (code) record.code = code;
|
|
165
152
|
return record;
|
|
166
153
|
}
|
|
167
|
-
function attachDefinition(
|
|
154
|
+
function attachDefinition(presentation, descriptor) {
|
|
168
155
|
const source = descriptor.metadata?.source,
|
|
169
156
|
category = descriptor.category ?? descriptor.metadata?.category;
|
|
170
157
|
if (!source) return;
|
|
@@ -179,33 +166,30 @@ function attachDefinition(document, descriptor) {
|
|
|
179
166
|
if (!id) return;
|
|
180
167
|
if (
|
|
181
168
|
catalogs[kind]?.some((record) => record.id === id) ||
|
|
182
|
-
|
|
169
|
+
presentation.catalogs?.[kind]?.records?.some((record) => record.id === id)
|
|
183
170
|
)
|
|
184
171
|
return;
|
|
185
172
|
let record;
|
|
186
173
|
if (kind === "layouts") {
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
"text";
|
|
174
|
+
// A gallery layout item is a layout record: its placeholders, design hints and composition are the
|
|
175
|
+
// record's own. Only an item that declares no placeholders gets the default title and text.
|
|
176
|
+
const placeholders = Array.isArray(source.placeholders)
|
|
177
|
+
? source.placeholders
|
|
178
|
+
.filter((placeholder) => typeof placeholder?.type === "string")
|
|
179
|
+
.map((placeholder) => ({ type: placeholder.type }))
|
|
180
|
+
: [{ type: "title" }, { type: "text" }];
|
|
181
|
+
const plain = (value) =>
|
|
182
|
+
value && typeof value === "object" && !Array.isArray(value)
|
|
183
|
+
? structuredClone(value)
|
|
184
|
+
: undefined;
|
|
194
185
|
record = {
|
|
195
186
|
$schema: "https://openpresentation.org/schema/opf-layout/v1",
|
|
196
187
|
id,
|
|
197
188
|
name: source.label ?? source.name ?? id,
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
...(n > 1
|
|
203
|
-
? {
|
|
204
|
-
composition: {
|
|
205
|
-
mode: n <= 3 ? "row" : "grid",
|
|
206
|
-
...(n > 3 ? { columns: 2 } : {}),
|
|
207
|
-
},
|
|
208
|
-
}
|
|
189
|
+
...(plain(source.design) ? { design: plain(source.design) } : {}),
|
|
190
|
+
placeholders,
|
|
191
|
+
...(plain(source.composition)
|
|
192
|
+
? { composition: plain(source.composition) }
|
|
209
193
|
: {}),
|
|
210
194
|
};
|
|
211
195
|
} else if (kind === "fontSchemes") record = fontSchemeRecord(source, id);
|
|
@@ -215,10 +199,10 @@ function attachDefinition(document, descriptor) {
|
|
|
215
199
|
$schema: `https://openpresentation.org/schema/opf-${kind === "themes" ? "theme" : "color-scheme"}/v1`,
|
|
216
200
|
};
|
|
217
201
|
}
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
202
|
+
presentation.catalogs ??= {};
|
|
203
|
+
presentation.catalogs[kind] ??= { records: [] };
|
|
204
|
+
presentation.catalogs[kind].records ??= [];
|
|
205
|
+
presentation.catalogs[kind].records.push(record);
|
|
222
206
|
}
|
|
223
207
|
export async function loadOpfGalleryItem(item, { gallery, ...options } = {}) {
|
|
224
208
|
const base = gallery ?? options.base;
|
|
@@ -234,10 +218,10 @@ export async function loadOpfGalleryItem(item, { gallery, ...options } = {}) {
|
|
|
234
218
|
);
|
|
235
219
|
if (!descriptor) throw new Error("This gallery item was not found.");
|
|
236
220
|
}
|
|
237
|
-
const
|
|
238
|
-
if (!
|
|
221
|
+
const presentation = structuredClone(unwrapOpf(descriptor));
|
|
222
|
+
if (!presentation?.slides)
|
|
239
223
|
throw new Error("This gallery item does not include a presentation.");
|
|
240
|
-
attachDefinition(
|
|
224
|
+
attachDefinition(presentation, descriptor);
|
|
241
225
|
// PPTX.gallery's older descriptors omit inline layout records. Resolve only named
|
|
242
226
|
// layout references from its known, same-origin registry; no arbitrary dependency crawl.
|
|
243
227
|
if (
|
|
@@ -246,13 +230,13 @@ export async function loadOpfGalleryItem(item, { gallery, ...options } = {}) {
|
|
|
246
230
|
) {
|
|
247
231
|
const missing = [
|
|
248
232
|
...new Set(
|
|
249
|
-
|
|
233
|
+
presentation.slides
|
|
250
234
|
.map((slide) => slide.layout)
|
|
251
235
|
.filter(
|
|
252
236
|
(id) =>
|
|
253
237
|
typeof id === "string" &&
|
|
254
238
|
!catalogs.layouts.some((record) => record.id === id) &&
|
|
255
|
-
!
|
|
239
|
+
!presentation.catalogs?.layouts?.records?.some(
|
|
256
240
|
(record) => record.id === id,
|
|
257
241
|
),
|
|
258
242
|
),
|
|
@@ -269,9 +253,9 @@ export async function loadOpfGalleryItem(item, { gallery, ...options } = {}) {
|
|
|
269
253
|
`/registry/layouts/${id}.json`,
|
|
270
254
|
{ ...options, base: url, origin: new URL(url).origin },
|
|
271
255
|
);
|
|
272
|
-
attachDefinition(
|
|
256
|
+
attachDefinition(presentation, dependency);
|
|
273
257
|
}
|
|
274
258
|
}
|
|
275
|
-
assertOpf(
|
|
276
|
-
return
|
|
259
|
+
assertOpf(presentation);
|
|
260
|
+
return presentation;
|
|
277
261
|
}
|