@openpresentation/opf-editor 0.12.1 → 0.15.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/README.md +122 -80
- package/dist/annotations.d.ts +16 -16
- package/dist/annotations.js +71 -79
- package/dist/assets.d.ts +7 -7
- package/dist/assets.js +16 -16
- package/dist/background-options.d.ts +12 -5
- package/dist/background-options.js +44 -19
- package/dist/block-controls.js +7 -7
- package/dist/block-convert.d.ts +7 -7
- package/dist/block-convert.js +22 -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 +30 -31
- package/dist/canvas.js +64 -68
- package/dist/catalogs.d.ts +48 -0
- package/dist/catalogs.js +217 -0
- package/dist/chart-data.d.ts +10 -10
- package/dist/chart-data.js +21 -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 +23 -0
- package/dist/color-authoring.js +1 -1
- package/dist/content-actions.d.ts +17 -17
- package/dist/content-actions.js +74 -73
- package/dist/content-controls.js +21 -21
- package/dist/data-grid.js +7 -8
- package/dist/data.d.ts +4 -4
- package/dist/data.js +5 -5
- package/dist/dataset-refs.d.ts +2 -2
- package/dist/dataset-refs.js +5 -5
- package/dist/design-controls.d.ts +12 -5
- package/dist/design-controls.js +312 -111
- package/dist/design-options.d.ts +20 -18
- package/dist/design-options.js +91 -99
- package/dist/edit-helpers.js +8 -7
- package/dist/exact-source.js +5 -5
- package/dist/export.d.ts +7 -7
- package/dist/export.js +48 -17
- package/dist/find-panel.js +5 -5
- package/dist/find-replace.d.ts +3 -3
- package/dist/find-replace.js +23 -20
- package/dist/font-gate.js +49 -58
- package/dist/galleries.js +50 -101
- package/dist/grid-model.d.ts +25 -27
- package/dist/grid-model.js +92 -102
- package/dist/image-crop.d.ts +4 -4
- package/dist/image-crop.js +28 -29
- package/dist/image-cropper.js +3 -3
- package/dist/image-options.d.ts +45 -0
- package/dist/image-options.js +195 -0
- package/dist/index.d.ts +48 -31
- package/dist/index.js +154 -126
- package/dist/json-editor.js +2 -2
- package/dist/json-menu.js +2 -2
- package/dist/json-options.d.ts +3 -3
- package/dist/json-options.js +44 -54
- 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 +160 -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 +26 -6
- package/dist/slide-manager.d.ts +2 -2
- package/dist/slide-manager.js +19 -19
- package/dist/slides.d.ts +21 -21
- package/dist/slides.js +104 -101
- package/dist/svelte.js +2 -3
- package/dist/switches.d.ts +37 -22
- package/dist/switches.js +282 -171
- package/dist/table-options.d.ts +8 -8
- package/dist/table-options.js +28 -27
- 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 +15 -4
- package/dist/transfer.js +107 -161
- package/package.json +12 -6
- package/dist/font-defaults.js +0 -25
package/dist/export.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { RenderSvgOptions } from "@openpresentation/opf-render";
|
|
2
|
-
import type {
|
|
2
|
+
import type { EditorFonts } from "./canvas.js";
|
|
3
3
|
|
|
4
4
|
export type ExportFormat = "pdf" | "png" | "svg";
|
|
5
5
|
export declare const EXPORT_FORMATS: Readonly<Record<ExportFormat, { extension: string; type: string; label: string }>>;
|
|
@@ -44,12 +44,12 @@ export interface ExportOptions {
|
|
|
44
44
|
pdfMode?: "vector" | "raster";
|
|
45
45
|
/** PNG pixel density (and the raster PDF's), 1 to 4; default 2. */
|
|
46
46
|
scale?: number;
|
|
47
|
-
/** The options the host draws its preview with (`
|
|
47
|
+
/** The options the host draws its preview with (`catalogs`, `date`, ...); the fonts go in `fonts`. */
|
|
48
48
|
renderOptions?: RenderSvgOptions;
|
|
49
|
-
/**
|
|
50
|
-
|
|
51
|
-
/** The
|
|
52
|
-
|
|
49
|
+
/** Host catalogs (merged with `renderOptions.catalogs`). The deck is embedded first (core `embed`), like a saved file. */
|
|
50
|
+
catalogs?: readonly import("@openpresentation/opf").Catalog[];
|
|
51
|
+
/** The renderer's fonts handle (`loadFonts()`): it measures the text, loads the faces the deck needs before anything is drawn, and its registry's faces are what gets embedded. */
|
|
52
|
+
fonts?: EditorFonts;
|
|
53
53
|
/** Faces to embed instead of the registry's. */
|
|
54
54
|
embeddedFonts?: Array<Record<string, unknown>>;
|
|
55
55
|
/** PDF document properties beyond the title and subject taken from the deck. */
|
|
@@ -71,7 +71,7 @@ export interface ExportResult {
|
|
|
71
71
|
slides: number[];
|
|
72
72
|
}
|
|
73
73
|
/**
|
|
74
|
-
* Draw the deck with the renderer the preview uses and convert it. Rejects with an error whose `code` is `export-aborted`, `export-no-slides`,
|
|
74
|
+
* Draw the deck with the renderer the preview uses and convert it. Rejects with an error whose `code` is `export-aborted`, `export-fonts-unlicensed` (the license rule left the PDF converter no face for some text), `export-no-slides`,
|
|
75
75
|
* `export-format`, `export-unavailable` (PDF and PNG need the renderer's `export-browser` entry), `fonts-unavailable` or a renderer code.
|
|
76
76
|
*/
|
|
77
77
|
export declare function exportDeck(deck: unknown, options: ExportOptions): Promise<ExportResult>;
|
package/dist/export.js
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
// RR-23: download a deck as PDF, PNG or SVG from the editor, next to the PowerPoint export. Everything runs in the page:
|
|
2
|
-
// the slides are drawn by the same renderer the preview uses (`
|
|
3
|
-
// a download is the preview, not a second layout. Fonts are the
|
|
4
|
-
//
|
|
2
|
+
// the slides are drawn by the same renderer the preview uses (`renderSvg`, with the fonts handle's text measurement), so
|
|
3
|
+
// a download is the preview, not a second layout. Fonts are the renderer's fonts handle's: the faces the document needs load through
|
|
4
|
+
// the handle first, and only faces its registry holds (bundled or hash-pinned, permissively licensed) are embedded, as
|
|
5
5
|
// @font-face data in each SVG and as subsets in the PDF. No system font is read and nothing is fetched at export time.
|
|
6
6
|
//
|
|
7
|
-
// const result = await exportDeck(editor.
|
|
7
|
+
// const result = await exportDeck(editor.presentation, { format: "pdf", fonts, renderOptions, signal, onProgress });
|
|
8
8
|
// // result.download is { name, type, bytes }: one PDF, one PNG or SVG, or a ZIP of the slides.
|
|
9
9
|
|
|
10
|
-
import {
|
|
10
|
+
import { renderSvg } from "@openpresentation/opf-render/svg";
|
|
11
|
+
import { mergeCatalogs, prepareSave } from "./catalogs.js";
|
|
12
|
+
import { fontGate } from "./font-gate.js";
|
|
11
13
|
import { createZip } from "./zip.js";
|
|
12
14
|
|
|
13
15
|
export const EXPORT_FORMATS = Object.freeze({
|
|
@@ -69,6 +71,19 @@ function exportError(code, message, cause) {
|
|
|
69
71
|
return error;
|
|
70
72
|
}
|
|
71
73
|
|
|
74
|
+
// The faces a PDF may embed: the same permissive-license list as the SVG (`embeddableFonts`), as the bytes the converter reads. The fonts
|
|
75
|
+
// handle itself is never given to `svgToPdf`: it would embed every face the registry holds, whatever its license.
|
|
76
|
+
function pdfFontData(faces) {
|
|
77
|
+
return faces.map((face) => {
|
|
78
|
+
const text = String(face.dataUrl ?? "");
|
|
79
|
+
const base64 = text.slice(text.indexOf(",") + 1);
|
|
80
|
+
const binary = atob(base64);
|
|
81
|
+
const data = new Uint8Array(binary.length);
|
|
82
|
+
for (let index = 0; index < binary.length; index += 1) data[index] = binary.charCodeAt(index);
|
|
83
|
+
return { family: face.family, data };
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
72
87
|
const fontLabel = (face) => `${face.family} ${face.weight}${face.italic ? " italic" : ""}`;
|
|
73
88
|
|
|
74
89
|
function slideOf(path) {
|
|
@@ -110,14 +125,21 @@ async function loadConverters(options) {
|
|
|
110
125
|
* - `slides`: "current" (with `slideIndex`), "all" (default; hidden slides only with `includeHidden`) or an array of slide numbers.
|
|
111
126
|
* - `pdfMode`: "vector" (default: selectable text, vector shapes) or "raster" (each slide an image).
|
|
112
127
|
* - `scale`: PNG pixel density, 1 to 4 (default 2); also the raster PDF's density.
|
|
113
|
-
* - `renderOptions`: the render options the host draws with (`
|
|
114
|
-
* - `
|
|
115
|
-
*
|
|
128
|
+
* - `renderOptions`: the render options the host draws with (`catalogs`, `date`, ...), the same as its preview.
|
|
129
|
+
* - `catalogs`: the host's registered catalogs (`Catalog[]`, merged with `renderOptions.catalogs`). The deck is embedded first (core's
|
|
130
|
+
* `embed`), so what is drawn is the self-contained document a save writes.
|
|
131
|
+
* - `fonts`: the renderer's fonts handle (`loadFonts()` from `@openpresentation/opf-render/fonts-browser`), the one the preview uses. Its
|
|
132
|
+
* `textMeasurement` lays the slides out, the faces the deck needs load through its `ensure` before anything is drawn, and its registry's
|
|
133
|
+
* faces are what gets embedded: the SVG carries the faces its slides draw, and the PDF embeds the registry's faces (script faces included).
|
|
134
|
+
* Both apply the same license rule (OFL-1.1, Apache-2.0, MIT or UFL-1.0): a face whose license text names none of them is left out of the SVG
|
|
135
|
+
* and the PDF and reported once as `export-font-license`. Text that needed it is drawn with another face the registry holds and
|
|
136
|
+
* reported by the converter (`pdf-font-substituted`, `pdf-glyph-missing`); when no face is left at all the PDF rejects with
|
|
137
|
+
* `export-fonts-unlicensed` (the converter's error is its `cause`).
|
|
116
138
|
* - `signal`, `onProgress({ stage, done, total, message })`, `onDiagnostic(diagnostic)`.
|
|
117
139
|
* Resolves `{ download, files, diagnostics, slides }`. Rejects with an error whose `code` is `export-aborted`,
|
|
118
|
-
* `export-no-slides`, `export-unavailable`, `fonts-unavailable` or a renderer code.
|
|
140
|
+
* `export-fonts-unlicensed`, `export-no-slides`, `export-unavailable`, `fonts-unavailable` or a renderer code.
|
|
119
141
|
*/
|
|
120
|
-
export async function exportDeck(
|
|
142
|
+
export async function exportDeck(input, options = {}) {
|
|
121
143
|
const format = EXPORT_FORMATS[options.format];
|
|
122
144
|
if (!format) throw exportError("export-format", `Choose a format: ${Object.keys(EXPORT_FORMATS).join(", ")}.`);
|
|
123
145
|
const { signal } = options;
|
|
@@ -125,28 +147,32 @@ export async function exportDeck(deck, options = {}) {
|
|
|
125
147
|
const note = (diagnostic) => { diagnostics.push(diagnostic); options.onDiagnostic?.(diagnostic); };
|
|
126
148
|
const progress = (stage, done, total, message) => options.onProgress?.({ stage, done, total, message });
|
|
127
149
|
const check = () => { if (signal?.aborted) throw exportError("export-aborted", "The export was cancelled."); };
|
|
128
|
-
const indexes = slidesToExport(
|
|
150
|
+
const indexes = slidesToExport(input, options);
|
|
129
151
|
if (!indexes.length) throw exportError("export-no-slides", "There are no slides to export.");
|
|
130
152
|
const selected = new Set(indexes);
|
|
131
153
|
const keep = (diagnostic) => diagnostic.slide === undefined || selected.has(diagnostic.slide);
|
|
132
154
|
const scale = Math.min(MAX_PNG_SCALE, Math.max(1, Number(options.scale) || 2));
|
|
133
|
-
const
|
|
155
|
+
const catalogs = mergeCatalogs(options.catalogs, options.renderOptions?.catalogs);
|
|
156
|
+
const renderOptions = { ...(options.renderOptions ?? {}), catalogs };
|
|
157
|
+
const deck = prepareSave(input, { catalogs }).document;
|
|
134
158
|
|
|
135
159
|
check();
|
|
136
|
-
|
|
160
|
+
const gate = fontGate(options.fonts);
|
|
161
|
+
if (gate?.pending(deck, renderOptions).length) {
|
|
137
162
|
progress("fonts", 0, 1, "Loading fonts…");
|
|
138
|
-
try { await
|
|
163
|
+
try { await gate.ensure(deck, { signal, renderOptions }); }
|
|
139
164
|
catch (error) { if (signal?.aborted) throw exportError("export-aborted", "The export was cancelled."); throw error; }
|
|
140
165
|
}
|
|
141
166
|
check();
|
|
142
167
|
const converters = format.extension === "svg" ? null : await loadConverters(options);
|
|
143
168
|
|
|
144
|
-
const
|
|
169
|
+
const dropped = [];
|
|
170
|
+
const embeddedFonts = options.embeddedFonts ?? embeddableFonts(options.fonts?.registry, (face) => { dropped.push(face); note({ code: "export-font-license", severity: "warning", message: `${fontLabel(face)} is not embedded: its license is not one of OFL-1.1, Apache-2.0, MIT or UFL-1.0.`, family: face.family }); });
|
|
145
171
|
progress("render", 0, indexes.length, "Drawing slides…");
|
|
146
172
|
await yieldToHost();
|
|
147
|
-
const rendered =
|
|
173
|
+
const rendered = renderSvg(deck, {
|
|
148
174
|
...renderOptions,
|
|
149
|
-
embeddedFonts,
|
|
175
|
+
fonts: { textMeasurement: options.fonts?.textMeasurement, embeddedFonts },
|
|
150
176
|
// The PDF reports problems by element path; the SVG and PNG are the plain drawing.
|
|
151
177
|
trace: format.extension === "pdf",
|
|
152
178
|
onDiagnostic: (diagnostic) => {
|
|
@@ -186,12 +212,17 @@ export async function exportDeck(deck, options = {}) {
|
|
|
186
212
|
scale,
|
|
187
213
|
metadata,
|
|
188
214
|
signal,
|
|
215
|
+
fontData: pdfFontData(embeddedFonts),
|
|
189
216
|
...(options.fallbackFamily ? { defaultFontFamily: options.fallbackFamily } : {}),
|
|
190
217
|
onDiagnostic: (diagnostic) => { const described = describeDiagnostic(diagnostic, "pdf"); if (keep(described)) note(described); },
|
|
191
218
|
onProgress: ({ page, pages }) => progress("convert", page, pages, `Page ${page} of ${pages}`),
|
|
192
219
|
});
|
|
193
220
|
} catch (error) {
|
|
194
221
|
if (signal?.aborted) throw exportError("export-aborted", "The export was cancelled.");
|
|
222
|
+
// The license rule can leave the converter with no face for some text (every face of a family was dropped): say why, not only what.
|
|
223
|
+
if (dropped.length && /font face/i.test(String(error?.message))) {
|
|
224
|
+
throw exportError("export-fonts-unlicensed", `The PDF could not be written: ${dropped.map(fontLabel).join(", ")} ${dropped.length === 1 ? "is" : "are"} not embedded because the license is not one of OFL-1.1, Apache-2.0, MIT or UFL-1.0, and no other face is available. ${error.message}`, error);
|
|
225
|
+
}
|
|
195
226
|
throw error;
|
|
196
227
|
}
|
|
197
228
|
files = [{ name: exportFileName(deck, "pdf"), type: format.type, bytes }];
|
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;
|
|
@@ -104,6 +106,7 @@ function payloadFields(fields, state, base, payload, prefix = "") {
|
|
|
104
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");
|
|
105
107
|
}));
|
|
106
108
|
}
|
|
109
|
+
if (isObject(payload.chart)) field(fields, state, at("chart", "alt"), payload.chart.alt, `${prefix}Chart alt text`, "alt");
|
|
107
110
|
const table = payload.table;
|
|
108
111
|
if (isObject(table)) {
|
|
109
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}`)));
|
|
@@ -131,27 +134,27 @@ function furnitureFields(fields, state, base, design) {
|
|
|
131
134
|
* the deck, `pointer` is the JSON pointer of the string or run array, and `runs` says it is a run array. Options:
|
|
132
135
|
* `notes: false` leaves speaker notes out, `slideIndex` limits the list to one slide.
|
|
133
136
|
*/
|
|
134
|
-
export function collectSearchFields(
|
|
137
|
+
export function collectSearchFields(presentation, options = {}) {
|
|
135
138
|
const fields = [];
|
|
136
|
-
if (!isObject(
|
|
139
|
+
if (!isObject(presentation)) return fields;
|
|
137
140
|
const only = Number.isInteger(options.slideIndex) ? options.slideIndex : undefined;
|
|
138
141
|
if (only === undefined) {
|
|
139
142
|
const deck = { slideIndex: -1 };
|
|
140
|
-
field(fields, deck, ["name"],
|
|
141
|
-
field(fields, deck, ["description"],
|
|
142
|
-
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);
|
|
143
146
|
}
|
|
144
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).
|
|
145
|
-
if (only === undefined && isObject(
|
|
148
|
+
if (only === undefined && isObject(presentation.datasets)) {
|
|
146
149
|
const deck = { slideIndex: -1 };
|
|
147
|
-
for (const [id, dataset] of Object.entries(
|
|
150
|
+
for (const [id, dataset] of Object.entries(presentation.datasets)) {
|
|
148
151
|
if (!isObject(dataset) || !Array.isArray(dataset.rows)) continue;
|
|
149
152
|
dataset.rows.forEach((row, r) => Array.isArray(row) && row.forEach((value, c) => {
|
|
150
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");
|
|
151
154
|
}));
|
|
152
155
|
}
|
|
153
156
|
}
|
|
154
|
-
(Array.isArray(
|
|
157
|
+
(Array.isArray(presentation.slides) ? presentation.slides : []).forEach((slide, slideIndex) => {
|
|
155
158
|
if (only !== undefined && slideIndex !== only) return;
|
|
156
159
|
if (!isObject(slide)) return;
|
|
157
160
|
const state = { slideIndex };
|
|
@@ -203,12 +206,12 @@ function snippet(text, start, end) {
|
|
|
203
206
|
* are offsets into the field's text. Options: those of `compileSearch`, plus `notes` and `slideIndex` of
|
|
204
207
|
* `collectSearchFields`. Matches that are empty (a regular expression that matches nothing) are skipped.
|
|
205
208
|
*/
|
|
206
|
-
export function findMatches(
|
|
209
|
+
export function findMatches(presentation, query, options = {}) {
|
|
207
210
|
const compiled = compileSearch(query, options);
|
|
208
211
|
const result = { matches: [], fieldCount: 0, slideCount: 0, truncated: false, error: compiled.error };
|
|
209
212
|
if (!compiled.regex) return result;
|
|
210
213
|
const slides = new Set();
|
|
211
|
-
const fields = collectSearchFields(
|
|
214
|
+
const fields = collectSearchFields(presentation, options);
|
|
212
215
|
outer: for (const item of fields) {
|
|
213
216
|
compiled.regex.lastIndex = 0;
|
|
214
217
|
let found;
|
|
@@ -320,8 +323,8 @@ export function applyTextEdits(value, edits) {
|
|
|
320
323
|
return result;
|
|
321
324
|
}
|
|
322
325
|
|
|
323
|
-
function readField(
|
|
324
|
-
const value = getValueAtPath(
|
|
326
|
+
function readField(presentation, match) {
|
|
327
|
+
const value = getValueAtPath(presentation, match.pointer);
|
|
325
328
|
return typeof value === "string" || Array.isArray(value) ? value : undefined;
|
|
326
329
|
}
|
|
327
330
|
|
|
@@ -330,7 +333,7 @@ function readField(document, match) {
|
|
|
330
333
|
* `{ patches, count, fieldCount }`; a replacement that leaves a field unchanged adds no patch. Throws `stale-match`
|
|
331
334
|
* when a match no longer reads the same text in the document.
|
|
332
335
|
*/
|
|
333
|
-
export function planReplace(
|
|
336
|
+
export function planReplace(presentation, matches, replacement, options = {}) {
|
|
334
337
|
const byField = new Map();
|
|
335
338
|
for (const match of matches) {
|
|
336
339
|
if (!byField.has(match.pointer)) byField.set(match.pointer, []);
|
|
@@ -339,7 +342,7 @@ export function planReplace(document, matches, replacement, options = {}) {
|
|
|
339
342
|
const patches = [];
|
|
340
343
|
let count = 0;
|
|
341
344
|
for (const [pointer, list] of byField) {
|
|
342
|
-
const value = readField(
|
|
345
|
+
const value = readField(presentation, list[0]);
|
|
343
346
|
const text = value === undefined ? undefined : textOf(value);
|
|
344
347
|
const ordered = [...list].sort((a, b) => a.start - b.start);
|
|
345
348
|
if (text === undefined || ordered.some((match) => text.slice(match.start, match.end) !== match.text)) {
|
|
@@ -353,8 +356,8 @@ export function planReplace(document, matches, replacement, options = {}) {
|
|
|
353
356
|
return { patches, count, fieldCount: byField.size };
|
|
354
357
|
}
|
|
355
358
|
|
|
356
|
-
function search(
|
|
357
|
-
const found = findMatches(
|
|
359
|
+
function search(presentation, query, options) {
|
|
360
|
+
const found = findMatches(presentation, query, options);
|
|
358
361
|
if (found.error) throw new OPFEditorError("invalid-search", found.error, {});
|
|
359
362
|
return found;
|
|
360
363
|
}
|
|
@@ -364,8 +367,8 @@ function search(document, query, options) {
|
|
|
364
367
|
* the session's result (or null when nothing changed). One undo restores every replaced field.
|
|
365
368
|
*/
|
|
366
369
|
export function replaceAll(editor, query, replacement, options = {}) {
|
|
367
|
-
const found = search(editor.
|
|
368
|
-
const plan = planReplace(editor.
|
|
370
|
+
const found = search(editor.presentation, query, options);
|
|
371
|
+
const plan = planReplace(editor.presentation, found.matches, String(replacement ?? ""), options);
|
|
369
372
|
if (!plan.patches.length) return { count: 0, fieldCount: 0, change: null };
|
|
370
373
|
const change = editor.applyPatch(plan.patches, { source: "find-replace", rejectInvalid: true, replaced: plan.count });
|
|
371
374
|
return { count: plan.count, fieldCount: plan.fieldCount, change };
|
|
@@ -376,10 +379,10 @@ export function replaceAll(editor, query, replacement, options = {}) {
|
|
|
376
379
|
* current document first: `stale-match` is thrown when the text changed underneath it.
|
|
377
380
|
*/
|
|
378
381
|
export function replaceMatch(editor, match, query, replacement, options = {}) {
|
|
379
|
-
const found = search(editor.
|
|
382
|
+
const found = search(editor.presentation, query, options);
|
|
380
383
|
const current = found.matches.find((candidate) => candidate.pointer === match.pointer && candidate.start === match.start && candidate.end === match.end && candidate.text === match.text);
|
|
381
384
|
if (!current) throw new OPFEditorError("stale-match", "That match is no longer in the document. Search again.", { path: match.path });
|
|
382
|
-
const plan = planReplace(editor.
|
|
385
|
+
const plan = planReplace(editor.presentation, [current], String(replacement ?? ""), options);
|
|
383
386
|
if (!plan.patches.length) return { count: 0, change: null };
|
|
384
387
|
const change = editor.applyPatch(plan.patches, { source: "find-replace", rejectInvalid: true, replaced: 1 });
|
|
385
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
|
},
|