@openpresentation/opf-editor 0.14.2 → 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 +60 -29
- package/dist/assets.js +1 -1
- package/dist/background-options.d.ts +10 -3
- package/dist/background-options.js +38 -13
- package/dist/canvas.d.ts +8 -1
- package/dist/canvas.js +12 -9
- package/dist/catalogs.d.ts +48 -0
- package/dist/catalogs.js +217 -0
- package/dist/checks.js +9 -7
- package/dist/content-actions.d.ts +2 -2
- package/dist/content-actions.js +3 -3
- package/dist/content-controls.js +9 -9
- package/dist/design-controls.d.ts +12 -5
- package/dist/design-controls.js +260 -76
- package/dist/design-options.d.ts +4 -4
- package/dist/design-options.js +6 -26
- package/dist/export.d.ts +2 -0
- package/dist/export.js +8 -3
- package/dist/galleries.js +32 -67
- package/dist/image-crop.js +8 -9
- package/dist/image-options.d.ts +45 -0
- package/dist/image-options.js +195 -0
- package/dist/index.d.ts +25 -8
- package/dist/index.js +91 -47
- 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 +30 -41
- package/dist/react.js +1 -1
- package/dist/review-panel.js +8 -2
- package/dist/schema-inspector.js +1 -1
- package/dist/schema.js +1 -1
- package/dist/slide-manager.js +1 -1
- package/dist/slides.d.ts +1 -1
- package/dist/slides.js +6 -5
- package/dist/svelte.js +1 -2
- package/dist/switches.d.ts +31 -16
- package/dist/switches.js +191 -90
- package/dist/transfer.d.ts +12 -1
- package/dist/transfer.js +87 -131
- package/package.json +9 -5
package/dist/index.js
CHANGED
|
@@ -2,15 +2,37 @@ import { paginateSlide } from "@openpresentation/opf/pagination";
|
|
|
2
2
|
import { OPFPatchError, applyPatch as applyCorePatch, applyPatchWithInverse, formatPointer, invertPatch, jsonEqual, parsePointer, readPointer } from "@openpresentation/opf/patch";
|
|
3
3
|
import { collectReservedPresentationIds } from "./presentation-ids.js";
|
|
4
4
|
import { composeSlide } from "@openpresentation/opf/composition";
|
|
5
|
-
import { catalogKinds,
|
|
5
|
+
import { catalogKinds, resolveSlideContext, stats } from "@openpresentation/opf";
|
|
6
6
|
import { checkFormat, errorFindings } from "./checks.js";
|
|
7
|
+
import { forkEditedRecords, forkNotice, listCatalogRecords, mergeCatalogs } from "./catalogs.js";
|
|
8
|
+
|
|
9
|
+
export {
|
|
10
|
+
REFERENCE_FINDING_CODES,
|
|
11
|
+
applyCatalogUpdate,
|
|
12
|
+
catalogRecordAt,
|
|
13
|
+
catalogRecordLabel,
|
|
14
|
+
catalogsFor,
|
|
15
|
+
checkCatalogUpdates,
|
|
16
|
+
editedCatalogRecords,
|
|
17
|
+
FORKED_RECORD_KINDS,
|
|
18
|
+
forkEditedRecords,
|
|
19
|
+
forkNotice,
|
|
20
|
+
listCatalogRecords,
|
|
21
|
+
mergeCatalogs,
|
|
22
|
+
moveToCustom,
|
|
23
|
+
prepareMoveToCustom,
|
|
24
|
+
prepareSave,
|
|
25
|
+
referenceFindings,
|
|
26
|
+
saveDocument,
|
|
27
|
+
} from "./catalogs.js";
|
|
7
28
|
|
|
8
29
|
// The options `composeSlide` and `paginateSlide` take for one slide of the open deck. Core's `resolveSlideContext` resolves the
|
|
9
30
|
// slide's canvas, layout, theme, colour scheme and font families the one way every engine does (slide design, deck design,
|
|
10
|
-
// theme, default), so the editor measures and composes what the renderer draws
|
|
11
|
-
// renderer's fonts handle (its `textMeasurement` measures); `options.
|
|
31
|
+
// theme, engine default), with the session's registered catalogs, so the editor measures and composes what the renderer draws
|
|
32
|
+
// and the exporter writes. `options.fonts` is the renderer's fonts handle (its `textMeasurement` measures); `options.catalogs`
|
|
33
|
+
// adds catalogs for this call; `options.onDiagnostic` hears each diagnostic (`unresolved-reference`);
|
|
12
34
|
// any other option overrides the resolved one (`layout`, ...).
|
|
13
|
-
function slideContext(presentation, slideIndex, { fonts, catalogs, onDiagnostic, ...overrides } = {}) {
|
|
35
|
+
function slideContext(presentation, slideIndex, catalogs, { fonts, catalogs: _extra, onDiagnostic, ...overrides } = {}) {
|
|
14
36
|
if (!Number.isInteger(slideIndex) || !presentation.slides?.[slideIndex]) throw new OPFEditorError("slide-index-out-of-range", "Slide index is out of range.");
|
|
15
37
|
const { options, diagnostics } = resolveSlideContext(presentation, slideIndex, { fonts, catalogs });
|
|
16
38
|
for (const diagnostic of diagnostics) onDiagnostic?.(diagnostic);
|
|
@@ -123,7 +145,10 @@ export function deckStats(editor, options) {
|
|
|
123
145
|
|
|
124
146
|
export function createEditorSession(input, options = {}) {
|
|
125
147
|
let presentation = parseInput(input);
|
|
126
|
-
|
|
148
|
+
// The host's registered catalogs (FA-23): one list, handed to every core call, the renderer and the exporter.
|
|
149
|
+
let catalogs = mergeCatalogs(options.catalogs);
|
|
150
|
+
const check = (document) => checkFormat(document, { catalogs });
|
|
151
|
+
let validation = check(presentation);
|
|
127
152
|
const undoStack = [];
|
|
128
153
|
const redoStack = [];
|
|
129
154
|
const listeners = new Set();
|
|
@@ -138,7 +163,7 @@ export function createEditorSession(input, options = {}) {
|
|
|
138
163
|
|
|
139
164
|
function commitPatch(operations, meta = {}) {
|
|
140
165
|
assertPatchOperations(operations);
|
|
141
|
-
|
|
166
|
+
let patches = operations.map(normalizeOperation);
|
|
142
167
|
const before = presentation;
|
|
143
168
|
let next, inversePatches;
|
|
144
169
|
try {
|
|
@@ -146,7 +171,24 @@ export function createEditorSession(input, options = {}) {
|
|
|
146
171
|
} catch (error) {
|
|
147
172
|
throw editorPatchError(error);
|
|
148
173
|
}
|
|
149
|
-
|
|
174
|
+
// Owner rule (FA-23): an edit of a record under catalogs.default or a named group forks it into catalogs.custom (every reference
|
|
175
|
+
// follows, the original is dropped), in this same undo step. Catalog updates and moves change catalog records on purpose.
|
|
176
|
+
let forks = [];
|
|
177
|
+
if (meta.catalogRecordEdit !== "in-place") {
|
|
178
|
+
const forked = forkEditedRecords(before, next, { catalogs, forkIds: meta.forkIds });
|
|
179
|
+
if (forked.forks.length) {
|
|
180
|
+
forks = forked.forks;
|
|
181
|
+
for (const key of new Set([...Object.keys(next), ...Object.keys(forked.document)]))
|
|
182
|
+
if (!jsonEqual(next[key], forked.document[key])) patches = [...patches, forked.document[key] === undefined ? { op: "remove", path: formatPointer([key]) } : { op: Object.hasOwn(next, key) ? "replace" : "add", path: formatPointer([key]), value: clone(forked.document[key]) }];
|
|
183
|
+
try {
|
|
184
|
+
({ presentation: next, inverse: inversePatches } = applyPatchWithInverse(before, patches));
|
|
185
|
+
} catch (error) {
|
|
186
|
+
throw editorPatchError(error);
|
|
187
|
+
}
|
|
188
|
+
meta = { ...meta, forked: forks, notice: forks.map(forkNotice).join(" ") };
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
const nextValidation = check(next);
|
|
150
192
|
|
|
151
193
|
if ((meta.rejectInvalid ?? rejectInvalid) && !nextValidation.valid) {
|
|
152
194
|
throw new OPFEditorError("invalid-opf-edit", "OPF edit produced an invalid document.", {
|
|
@@ -174,7 +216,8 @@ export function createEditorSession(input, options = {}) {
|
|
|
174
216
|
presentation: clone(presentation),
|
|
175
217
|
patches: clonePatchOperations(patches),
|
|
176
218
|
inversePatches: clonePatchOperations(inversePatches),
|
|
177
|
-
validation
|
|
219
|
+
validation,
|
|
220
|
+
...(forks.length ? { forked: forks, notice: meta.notice } : {})
|
|
178
221
|
};
|
|
179
222
|
}
|
|
180
223
|
|
|
@@ -185,6 +228,20 @@ export function createEditorSession(input, options = {}) {
|
|
|
185
228
|
get validation() {
|
|
186
229
|
return validation;
|
|
187
230
|
},
|
|
231
|
+
/** The catalogs the host registered (`Catalog[]`, frozen): the first is the host default. */
|
|
232
|
+
get catalogs() {
|
|
233
|
+
return catalogs;
|
|
234
|
+
},
|
|
235
|
+
/**
|
|
236
|
+
* Replace the registered catalogs (for example after the host loads a company catalog). The document is not changed; it is
|
|
237
|
+
* re-validated against the new list (reference findings depend on it) and one `catalogs` event tells every view to redraw.
|
|
238
|
+
*/
|
|
239
|
+
setCatalogs(next, meta = {}) {
|
|
240
|
+
catalogs = mergeCatalogs(next);
|
|
241
|
+
validation = check(presentation);
|
|
242
|
+
emit({ type: "catalogs", patches: [], validation, meta });
|
|
243
|
+
return catalogs;
|
|
244
|
+
},
|
|
188
245
|
get canUndo() {
|
|
189
246
|
return undoStack.length > 0;
|
|
190
247
|
},
|
|
@@ -218,13 +275,15 @@ export function createEditorSession(input, options = {}) {
|
|
|
218
275
|
});
|
|
219
276
|
},
|
|
220
277
|
composeSlide(slideIndex, options = {}) {
|
|
221
|
-
return composeSlide(presentation.slides?.[slideIndex], slideContext(presentation, slideIndex, options));
|
|
278
|
+
return composeSlide(presentation.slides?.[slideIndex], slideContext(presentation, slideIndex, mergeCatalogs(catalogs, options.catalogs), options));
|
|
222
279
|
},
|
|
223
280
|
paginateSlide(slideIndex, options = {}, meta = {}) {
|
|
281
|
+
const list = mergeCatalogs(catalogs, options.catalogs);
|
|
224
282
|
// Core pagination reads the measurement from `fonts`, so the resolved context hands it over that way.
|
|
225
|
-
const { textMeasurement, ...resolved } = slideContext(presentation, slideIndex, options);
|
|
283
|
+
const { textMeasurement, ...resolved } = slideContext(presentation, slideIndex, list, options);
|
|
226
284
|
const pagination = paginateSlide(presentation.slides[slideIndex], {
|
|
227
285
|
...resolved,
|
|
286
|
+
catalogs: list,
|
|
228
287
|
fonts: options.fonts ?? (textMeasurement ? { textMeasurement } : undefined),
|
|
229
288
|
reservedIds: collectReservedPresentationIds(presentation),
|
|
230
289
|
});
|
|
@@ -267,7 +326,7 @@ export function createEditorSession(input, options = {}) {
|
|
|
267
326
|
restoreState(state, meta = {}) {
|
|
268
327
|
if (!state || typeof state !== "object") throw new OPFEditorError("invalid-state", "restoreState needs { presentation, undo?, redo? }.");
|
|
269
328
|
const next = parseInput(state.presentation);
|
|
270
|
-
const nextValidation =
|
|
329
|
+
const nextValidation = check(next);
|
|
271
330
|
if ((meta.rejectInvalid ?? rejectInvalid) && !nextValidation.valid) {
|
|
272
331
|
throw new OPFEditorError("invalid-opf-edit", "The restored document is not valid OPF.", { issues: errorFindings(nextValidation) });
|
|
273
332
|
}
|
|
@@ -304,7 +363,7 @@ export function createEditorSession(input, options = {}) {
|
|
|
304
363
|
const entry = undoStack.pop();
|
|
305
364
|
if (!entry) return null;
|
|
306
365
|
presentation = applyJsonPatch(presentation, entry.inversePatches);
|
|
307
|
-
validation =
|
|
366
|
+
validation = check(presentation);
|
|
308
367
|
redoStack.push(entry);
|
|
309
368
|
emit({
|
|
310
369
|
type: "undo",
|
|
@@ -324,7 +383,7 @@ export function createEditorSession(input, options = {}) {
|
|
|
324
383
|
const entry = redoStack.pop();
|
|
325
384
|
if (!entry) return null;
|
|
326
385
|
presentation = applyJsonPatch(presentation, entry.patches);
|
|
327
|
-
validation =
|
|
386
|
+
validation = check(presentation);
|
|
328
387
|
undoStack.push(entry);
|
|
329
388
|
emit({
|
|
330
389
|
type: "redo",
|
|
@@ -415,29 +474,29 @@ export function createSvgTraceBinding(root, editor, options = {}) {
|
|
|
415
474
|
};
|
|
416
475
|
}
|
|
417
476
|
|
|
477
|
+
/**
|
|
478
|
+
* The records a catalog control can offer for `catalogKind`, from core's `catalogRecords`: the document's embedded records,
|
|
479
|
+
* then the registered catalogs' (`options.catalogs`, merged with `options.editor`'s). Each is the record itself.
|
|
480
|
+
*/
|
|
418
481
|
export function getCatalogRecords(catalogKind, options = {}) {
|
|
419
|
-
|
|
420
|
-
const presentationCatalog = options.presentation?.catalogs?.[catalogKind];
|
|
421
|
-
const explicitCatalog = options.catalogs?.[catalogKind] ?? options.catalogSources?.[catalogKind];
|
|
422
|
-
|
|
423
|
-
return firstRecords(presentationCatalog)
|
|
424
|
-
?? firstRecords(explicitCatalog)
|
|
425
|
-
?? firstRecords(bundledCatalogs[catalogKind])
|
|
426
|
-
?? [];
|
|
482
|
+
return getCatalogOptions(catalogKind, options).map((option) => option.record);
|
|
427
483
|
}
|
|
428
484
|
|
|
485
|
+
/**
|
|
486
|
+
* Picker options for `catalogKind`: `{ id, reference, label, record, group, source, origin }`, where `id` (= `reference`) is the
|
|
487
|
+
* reference to write, `id` or `name:id`. `options.presentation` is the document (default: `options.editor`'s), `options.catalogs`
|
|
488
|
+
* the host catalogs (merged after the session's registered ones).
|
|
489
|
+
*/
|
|
429
490
|
export function getCatalogOptions(catalogKind, options = {}) {
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
record
|
|
434
|
-
}));
|
|
491
|
+
assertCatalogKind(catalogKind);
|
|
492
|
+
const presentation = options.presentation ?? options.editor?.presentation ?? {};
|
|
493
|
+
return listCatalogRecords(presentation, catalogKind, { catalogs: mergeCatalogs(options.editor?.catalogs, options.catalogs) });
|
|
435
494
|
}
|
|
436
495
|
|
|
437
496
|
export function assertCatalogId(catalogKind, id, options = {}) {
|
|
438
497
|
assertCatalogKind(catalogKind);
|
|
439
498
|
if (typeof id !== "string" || id.length === 0) {
|
|
440
|
-
throw new OPFEditorError("invalid-catalog-id", "Catalog control values must be non-empty catalog
|
|
499
|
+
throw new OPFEditorError("invalid-catalog-id", "Catalog control values must be non-empty catalog references.", {
|
|
441
500
|
catalogKind,
|
|
442
501
|
id
|
|
443
502
|
});
|
|
@@ -445,7 +504,7 @@ export function assertCatalogId(catalogKind, id, options = {}) {
|
|
|
445
504
|
|
|
446
505
|
const optionsById = new Set(getCatalogOptions(catalogKind, options).map((option) => option.id));
|
|
447
506
|
if (!optionsById.has(id)) {
|
|
448
|
-
throw new OPFEditorError("unknown-catalog-id", `Unknown ${catalogKind} catalog
|
|
507
|
+
throw new OPFEditorError("unknown-catalog-id", `Unknown ${catalogKind} catalog reference: ${id}.`, {
|
|
449
508
|
catalogKind,
|
|
450
509
|
id
|
|
451
510
|
});
|
|
@@ -454,16 +513,16 @@ export function assertCatalogId(catalogKind, id, options = {}) {
|
|
|
454
513
|
return id;
|
|
455
514
|
}
|
|
456
515
|
|
|
516
|
+
/** Write the catalog reference `id` at `path` (an object reference keeps its overrides and takes the new `id`). */
|
|
457
517
|
export function setCatalogId(editor, path, catalogKind, id, meta = {}) {
|
|
458
518
|
assertEditorSession(editor);
|
|
459
519
|
const catalogId = assertCatalogId(catalogKind, id, {
|
|
460
520
|
presentation: meta.presentation ?? editor.presentation,
|
|
461
|
-
catalogs: meta.catalogs
|
|
462
|
-
catalogSources: meta.catalogSources
|
|
521
|
+
catalogs: mergeCatalogs(editor.catalogs, meta.catalogs)
|
|
463
522
|
});
|
|
464
523
|
const current = editor.get(path);
|
|
465
524
|
const value = catalogReferenceValue(current, catalogId);
|
|
466
|
-
const { catalogs
|
|
525
|
+
const { catalogs: _catalogs, presentation: _presentation, ...commitMeta } = meta;
|
|
467
526
|
return editor.set(path, value, {
|
|
468
527
|
...commitMeta,
|
|
469
528
|
catalogKind,
|
|
@@ -517,7 +576,7 @@ export function createCatalogSelect(editor, options) {
|
|
|
517
576
|
if (options?.label) select.setAttribute("aria-label", options.label);
|
|
518
577
|
|
|
519
578
|
for (const option of getCatalogOptions(catalogKind, {
|
|
520
|
-
|
|
579
|
+
catalogs: mergeCatalogs(editor.catalogs, options?.catalogs),
|
|
521
580
|
presentation: options?.presentation ?? editor.presentation
|
|
522
581
|
})) {
|
|
523
582
|
const optionElement = documentRef.createElement("option");
|
|
@@ -533,7 +592,6 @@ export function createCatalogSelect(editor, options) {
|
|
|
533
592
|
const onChange = () => {
|
|
534
593
|
setCatalogId(editor, path, catalogKind, select.value, {
|
|
535
594
|
catalogs: options?.catalogs,
|
|
536
|
-
catalogSources: options?.catalogSources,
|
|
537
595
|
presentation: options?.presentation,
|
|
538
596
|
source: "catalog-select"
|
|
539
597
|
});
|
|
@@ -677,20 +735,6 @@ function assertCatalogKind(catalogKind) {
|
|
|
677
735
|
}
|
|
678
736
|
}
|
|
679
737
|
|
|
680
|
-
function firstRecords(value) {
|
|
681
|
-
if (Array.isArray(value)) return value.filter(isCatalogRecord);
|
|
682
|
-
if (Array.isArray(value?.records)) return value.records.filter(isCatalogRecord);
|
|
683
|
-
return null;
|
|
684
|
-
}
|
|
685
|
-
|
|
686
|
-
function isCatalogRecord(record) {
|
|
687
|
-
return Boolean(record && typeof record === "object" && typeof record.id === "string");
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
function catalogLabel(record) {
|
|
691
|
-
return String(record.name ?? record.title ?? record.label ?? record.id);
|
|
692
|
-
}
|
|
693
|
-
|
|
694
738
|
function catalogReferenceValue(current, id) {
|
|
695
739
|
if (current && typeof current === "object" && !Array.isArray(current)) {
|
|
696
740
|
return { ...current, id };
|
package/dist/json-editor.js
CHANGED
|
@@ -103,7 +103,7 @@ function skipDelimiter(view, delimiter) {
|
|
|
103
103
|
}
|
|
104
104
|
function mountJsonCodeEditor(parent, options) {
|
|
105
105
|
const attributes = new Compartment(), indent = new Compartment();
|
|
106
|
-
let disposed=false, menu=null, catalogs=options.catalogs??
|
|
106
|
+
let disposed=false, menu=null, catalogs=options.catalogs??[], generation=0;
|
|
107
107
|
function closeOptions(){generation++;menu?.destroy();menu=null;}
|
|
108
108
|
async function openOptions(left,top){
|
|
109
109
|
closeOptions();
|
|
@@ -244,7 +244,7 @@ function mountJsonCodeEditor(parent, options) {
|
|
|
244
244
|
return {
|
|
245
245
|
api,
|
|
246
246
|
openOptions,
|
|
247
|
-
setCatalogs(value){catalogs=value??
|
|
247
|
+
setCatalogs(value){catalogs=value??[];closeOptions();},
|
|
248
248
|
update(code, invalid=false, field=null, describedBy) {
|
|
249
249
|
if(code!==api.getValue())closeOptions();
|
|
250
250
|
const current = view.state.doc.toString(), normalized=textInputMap(code).text;
|
package/dist/json-menu.js
CHANGED
|
@@ -26,9 +26,9 @@ export function mountJsonFieldMenu(parent,api,catalogs,left,top,onClose){
|
|
|
26
26
|
const error=add('p',undefined,'json-field-empty');error.setAttribute('role','alert');error.hidden=true;
|
|
27
27
|
if(isLayout){
|
|
28
28
|
const help=add('details',undefined,'json-field-catalog-help');add('summary','Where do these layouts come from?',undefined,help);
|
|
29
|
-
add('p','
|
|
29
|
+
add('p','Provided by app: records from the catalogs the host app registered. In this document: records embedded in your JSON, which take precedence. Nothing is fetched; saving embeds every record the document uses.',undefined,help);
|
|
30
30
|
}
|
|
31
|
-
add('p',context.unloadedSource?'Using
|
|
31
|
+
add('p',context.unloadedSource?'Using the embedded records: a catalog this document names is not registered here.':'Choose an option or press Escape to keep editing JSON.','json-field-footnote');
|
|
32
32
|
let selected=Math.max(0,context.options.findIndex(option=>option.value===context.value)),choices=[],disposed=false;
|
|
33
33
|
function close(restore=false){if(disposed)return;disposed=true;observer.disconnect();root.remove();doc.removeEventListener('pointerdown',outside);doc.removeEventListener('wheel',outside,true);doc.removeEventListener('touchmove',outside,true);win.removeEventListener('resize',resize);onClose();if(restore){api.focus();api.setSelection(...selection);}}
|
|
34
34
|
function outside(event){if(!root.contains(event.target))close();}
|
package/dist/json-options.d.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
/**
|
|
2
|
-
export type JsonCatalogContext =
|
|
1
|
+
/** The host's catalogs (core `Catalog[]`): records offered after the document's embedded ones. */
|
|
2
|
+
export type JsonCatalogContext = readonly import("@openpresentation/opf").Catalog[];
|
|
3
3
|
export type JsonOptionValue = string | number | boolean;
|
|
4
4
|
export interface JsonFieldOption {
|
|
5
5
|
value: JsonOptionValue;
|
|
6
6
|
label: string;
|
|
7
7
|
description?: string;
|
|
8
|
-
source: '
|
|
8
|
+
source: 'Loaded catalog' | 'Document catalog' | 'Schema' | 'Current value';
|
|
9
9
|
suggested?: boolean;
|
|
10
10
|
/** User-facing provenance; source remains stable for existing consumers. */
|
|
11
11
|
sourceLabel?: string;
|
package/dist/json-options.js
CHANGED
|
@@ -1,12 +1,23 @@
|
|
|
1
|
-
import { catalogKinds, catalogSchemaNames, schemas, validateCatalogRecord } from "@openpresentation/opf";
|
|
2
1
|
import { checkFormat, firstErrorMessage } from "./checks.js";
|
|
3
|
-
import {
|
|
2
|
+
import { listCatalogRecords } from "./catalogs.js";
|
|
4
3
|
import { schemaAtPath, schemaVariants } from "./schema.js";
|
|
5
4
|
import { applyEdits, findNodeAtLocation, findNodeAtOffset, getNodePath, modify, parseTree } from "jsonc-parser";
|
|
6
5
|
import { populateLayoutPlaceholders } from "./layout-placeholders.js";
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
// The catalog kind a field references (OPF 0.15: every content kind is `id` or `name:id`), read from where the field sits.
|
|
7
|
+
const ROOT_REFERENCES = { narrative: "narratives", audience: "audiences", purpose: "purposes", tone: "tones" };
|
|
8
|
+
const DESIGN_REFERENCES = { theme: "themes", colorScheme: "colorSchemes", fontScheme: "fontSchemes" };
|
|
9
|
+
function catalogAtPath(path) {
|
|
10
|
+
const parts = path.map(String);
|
|
11
|
+
// A reference's object form names its record with `id`; an audience array's entries are references too.
|
|
12
|
+
const field = parts.at(-1) === "id" ? parts.slice(0, -1) : parts;
|
|
13
|
+
const tail = /^\d+$/.test(field.at(-1) ?? "") && field.at(-2) === "audience" ? field.slice(0, -1) : field;
|
|
14
|
+
const key = tail.at(-1);
|
|
15
|
+
if (tail.length === 3 && tail[0] === "slides" && key === "layout") return "layouts";
|
|
16
|
+
if (tail.length === 1 && ROOT_REFERENCES[key]) return ROOT_REFERENCES[key];
|
|
17
|
+
if (DESIGN_REFERENCES[key] && ((tail.length === 2 && tail[0] === "design") || (tail.length === 4 && tail[0] === "slides" && tail[2] === "design"))) return DESIGN_REFERENCES[key];
|
|
18
|
+
// Inside an embedded record: a theme's colour or font scheme.
|
|
19
|
+
if (tail.length === 5 && tail[0] === "catalogs" && tail[2] === "themes" && (key === "colorScheme" || key === "fontScheme")) return DESIGN_REFERENCES[key];
|
|
20
|
+
return undefined;
|
|
10
21
|
}
|
|
11
22
|
// Compare declared placeholders, including multiplicity. This describes a layout
|
|
12
23
|
// contract, not a claim that arbitrary slide content will render without overflow.
|
|
@@ -28,9 +39,8 @@ function placeholderSummary(types) {
|
|
|
28
39
|
}).join(" + ");
|
|
29
40
|
}
|
|
30
41
|
const sourceDetails = {
|
|
31
|
-
"
|
|
32
|
-
"
|
|
33
|
-
"Document catalog": ["In this document", "Defined in this document’s catalogs. Takes precedence over app and standard records with the same ID."],
|
|
42
|
+
"Loaded catalog": ["Provided by app", "Records from a catalog this app registered; nothing is fetched from a URL. Saving embeds the record in the document."],
|
|
43
|
+
"Document catalog": ["In this document", "Embedded in this document’s catalogs. Takes precedence over the app’s records with the same reference."],
|
|
34
44
|
"Current value": ["Custom value", "The current value is not in the available catalogs."],
|
|
35
45
|
"Schema": ["OPF option", "Allowed by the OPF schema."],
|
|
36
46
|
};
|
|
@@ -64,7 +74,7 @@ function describeOptions(choices, records, current, catalog) {
|
|
|
64
74
|
}
|
|
65
75
|
// The installed schema identifies fields; a document field merely named "layout"
|
|
66
76
|
// inside arbitrary metadata is not mistaken for an OPF layout reference.
|
|
67
|
-
export function getJsonFieldContext(source, position, loadedCatalogs =
|
|
77
|
+
export function getJsonFieldContext(source, position, loadedCatalogs = []) {
|
|
68
78
|
let presentation;
|
|
69
79
|
try {
|
|
70
80
|
presentation = JSON.parse(source);
|
|
@@ -87,42 +97,21 @@ export function getJsonFieldContext(source, position, loadedCatalogs = {}) {
|
|
|
87
97
|
return null;
|
|
88
98
|
const schema = schemaAtPath(presentation, path.map(String));
|
|
89
99
|
const variants = schemaVariants(schema);
|
|
90
|
-
const
|
|
91
|
-
let catalog = catalogFor(description);
|
|
92
|
-
// Audience arrays inherit the catalog context from the array property.
|
|
93
|
-
if (!catalog && typeof path.at(-1) === "number")
|
|
94
|
-
catalog = catalogFor(String(schemaAtPath(presentation, path.slice(0, -1).map(String)).description ?? ""));
|
|
100
|
+
const catalog = catalogAtPath(path);
|
|
95
101
|
const options = new Map();
|
|
96
102
|
const records = new Map();
|
|
97
|
-
|
|
103
|
+
// A document group whose source no registered catalog matches: its records beyond the embedded ones are not available here.
|
|
104
|
+
const registered = new Set((Array.isArray(loadedCatalogs) ? loadedCatalogs : []).map(entry => entry?.source));
|
|
105
|
+
const unloadedSource = Boolean(catalog) && Object.entries(presentation.catalogs ?? {}).some(([name, group]) => name !== "custom" && group && typeof group === "object" && typeof group.source === "string" && !registered.has(group.source));
|
|
98
106
|
if (catalog && typeof node.value === "string") {
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
107
|
+
const hostCatalogs = Array.isArray(loadedCatalogs) ? loadedCatalogs : [];
|
|
108
|
+
// A draft whose own catalogs are malformed still offers the host's records; a malformed host list offers nothing.
|
|
109
|
+
const attempt = (document) => { try { return listCatalogRecords(document, catalog, { catalogs: hostCatalogs }); } catch { return undefined; } };
|
|
110
|
+
const available = attempt(presentation) ?? attempt({ ...presentation, catalogs: undefined }) ?? [];
|
|
111
|
+
for (const option of available) {
|
|
103
112
|
records.set(option.id, option.record);
|
|
104
|
-
options.set(option.id, { value: option.id, label: option.label, description:
|
|
105
|
-
source: "
|
|
106
|
-
}
|
|
107
|
-
for (const { value, source } of [...(Array.isArray(loadedCatalogs?.[catalog]) ? loadedCatalogs[catalog] : []).map(value => ({ value, source: "Loaded catalog" })),
|
|
108
|
-
...(Array.isArray(local?.records) ? local.records : []).map(value => ({ value, source: "Document catalog" }))]) {
|
|
109
|
-
if (!value || typeof value !== "object")
|
|
110
|
-
continue;
|
|
111
|
-
const record = value;
|
|
112
|
-
if (typeof record.id !== "string")
|
|
113
|
-
continue;
|
|
114
|
-
// Inline records may omit the standalone document's $schema marker.
|
|
115
|
-
// Supply it only for validation; never change the authored record.
|
|
116
|
-
const schemaId = schemas[catalogSchemaNames[catalog]].$id;
|
|
117
|
-
if (!validateCatalogRecord(catalog, { $schema: schemaId, ...record }).valid) {
|
|
118
|
-
options.delete(record.id);
|
|
119
|
-
records.delete(record.id);
|
|
120
|
-
continue;
|
|
121
|
-
}
|
|
122
|
-
records.set(record.id, record);
|
|
123
|
-
options.set(record.id, { value: record.id, label: typeof record.name === "string" ? record.name : record.id,
|
|
124
|
-
description: typeof record.description === "string" ? record.description : undefined,
|
|
125
|
-
source });
|
|
113
|
+
options.set(option.id, { value: option.id, label: option.label, description: option.description,
|
|
114
|
+
source: option.origin === "document" ? "Document catalog" : "Loaded catalog" });
|
|
126
115
|
}
|
|
127
116
|
}
|
|
128
117
|
else {
|
package/dist/react.js
CHANGED
|
@@ -62,7 +62,7 @@ export function createOPFReactComponents(React) {
|
|
|
62
62
|
const snapshot = useEditorSnapshot(editor);
|
|
63
63
|
const value = getValueAtPath(snapshot.presentation, path, "");
|
|
64
64
|
const currentId = value && typeof value === "object" ? value.id : value;
|
|
65
|
-
const options = catalogOptions ?? getCatalogOptions(catalogKind, { presentation: snapshot.presentation });
|
|
65
|
+
const options = catalogOptions ?? getCatalogOptions(catalogKind, { presentation: snapshot.presentation, catalogs: editor.catalogs });
|
|
66
66
|
|
|
67
67
|
return React.createElement(
|
|
68
68
|
"select",
|
package/dist/review-panel.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
// document state: every fix is an undoable edit through the editor session. Importing this module does not need a DOM;
|
|
7
7
|
// mounting does.
|
|
8
8
|
import { validate } from "@openpresentation/opf";
|
|
9
|
+
import { mergeCatalogs } from "./catalogs.js";
|
|
9
10
|
import {
|
|
10
11
|
applyReviewFix,
|
|
11
12
|
countFindings,
|
|
@@ -115,6 +116,11 @@ function summaryText(counts, scopeLabel) {
|
|
|
115
116
|
*/
|
|
116
117
|
export function createReviewPanel(container, options) {
|
|
117
118
|
const { editor, getSlideIndex, getValidateOptions, onGoTo, onFocusField, onStatus, onIgnoredChange, onChange, review } = options;
|
|
119
|
+
// Core resolves the deck's references with the session's registered catalogs (and any the host adds): the same list as the preview.
|
|
120
|
+
const validateOptions = () => {
|
|
121
|
+
const host = getValidateOptions?.(editor.presentation) ?? {};
|
|
122
|
+
return { ...host, catalogs: mergeCatalogs(editor.catalogs, host.catalogs) };
|
|
123
|
+
};
|
|
118
124
|
if (review !== undefined && typeof review !== "function") throw new TypeError("The Review panel's review hook must be a function (presentation, options) returning a FindingReport.");
|
|
119
125
|
if (!editor || typeof editor.subscribe !== "function") throw new Error("The Review panel needs an editor session created by createEditorSession.");
|
|
120
126
|
const doc = container.ownerDocument;
|
|
@@ -171,7 +177,7 @@ export function createReviewPanel(container, options) {
|
|
|
171
177
|
function validateNow() {
|
|
172
178
|
lastAuditError = undefined;
|
|
173
179
|
try {
|
|
174
|
-
coreReport = validate(editor.presentation,
|
|
180
|
+
coreReport = validate(editor.presentation, validateOptions());
|
|
175
181
|
} catch (error) {
|
|
176
182
|
coreReport = undefined;
|
|
177
183
|
lastAuditError = error;
|
|
@@ -204,7 +210,7 @@ export function createReviewPanel(container, options) {
|
|
|
204
210
|
if (!coreReport && !lastAuditError) validateNow();
|
|
205
211
|
renderList();
|
|
206
212
|
try {
|
|
207
|
-
const result = await review(editor.presentation, { signal: controller.signal, report: coreReport, validateOptions:
|
|
213
|
+
const result = await review(editor.presentation, { signal: controller.signal, report: coreReport, validateOptions: validateOptions() });
|
|
208
214
|
if (destroyed || hookRunning !== running) return;
|
|
209
215
|
if (!result || !Array.isArray(result.findings)) throw new TypeError("The review hook must resolve a FindingReport ({ valid, findings, counts }).");
|
|
210
216
|
hookRunning = undefined;
|
package/dist/schema-inspector.js
CHANGED
|
@@ -18,7 +18,7 @@ export function createSchemaInspector(container, {editor,path='',onDraft,onCommi
|
|
|
18
18
|
function showError(message){errorBox.textContent=message;}
|
|
19
19
|
function check(){
|
|
20
20
|
// The draft must be valid OPF (the format category, as every edit); unresolved references (an unknown variable, a missing asset) are listed as notes.
|
|
21
|
-
const report=validate(draft,{only:['format','references']});
|
|
21
|
+
const report=validate(draft,{only:['format','references'],...(editor?.catalogs?.length?{catalogs:editor.catalogs}:{})});
|
|
22
22
|
const errors=report.findings.filter(finding=>finding.severity==='error'&&finding.category==='format'),warnings=report.findings.filter(finding=>finding.category!=='format'&&finding.severity!=='info');
|
|
23
23
|
const validation={valid:!errors.length};
|
|
24
24
|
apply.disabled=!dirty||!validation.valid;
|
package/dist/schema.js
CHANGED
|
@@ -69,7 +69,7 @@ export function schemaAtPath(presentation, path, root = schemas.presentation) {
|
|
|
69
69
|
const parts=splitOpfPath(path);let current=root,value=presentation;
|
|
70
70
|
for(let index=0; index<parts.length;index++) {
|
|
71
71
|
const key=parts[index], resolved=activeSchema(current,value,root);
|
|
72
|
-
if(parts[0]==='catalogs' && parts[2]
|
|
72
|
+
if(parts[0]==='catalogs' && index===3 && catalogSchemaNames[parts[2]]) current=schemas[catalogSchemaNames[parts[2]]]??{};
|
|
73
73
|
else current=Array.isArray(value)||resolved.type==='array' ? resolved.items??{} : resolved.properties?.[key] ?? Object.entries(resolved.patternProperties??{}).find(([pattern])=>new RegExp(pattern).test(key))?.[1] ?? (typeof resolved.additionalProperties==='object'?resolved.additionalProperties:{});
|
|
74
74
|
value=value?.[key];
|
|
75
75
|
}
|
package/dist/slide-manager.js
CHANGED
|
@@ -482,7 +482,7 @@ export function createSlideManager(container, options) {
|
|
|
482
482
|
});
|
|
483
483
|
}
|
|
484
484
|
function layoutChoices() {
|
|
485
|
-
return options.layoutOptions ? options.layoutOptions(deck()) : listSwitchOptions(deck(), "layouts");
|
|
485
|
+
return options.layoutOptions ? options.layoutOptions(deck()) : listSwitchOptions(deck(), "layouts", { catalogs: editor.catalogs });
|
|
486
486
|
}
|
|
487
487
|
function openLayoutPicker() {
|
|
488
488
|
const choices = layoutChoices();
|
package/dist/slides.d.ts
CHANGED
|
@@ -48,7 +48,7 @@ export type MoveSectionMode = "adopt" | "keep" | string | null;
|
|
|
48
48
|
export interface AddSlideOptions {
|
|
49
49
|
/** The index the slide takes (default: the end). */
|
|
50
50
|
at?: number;
|
|
51
|
-
/** A
|
|
51
|
+
/** A layout reference (`id` or `name:id`); the layout's placeholders are added as empty slots. */
|
|
52
52
|
layout?: string;
|
|
53
53
|
title?: string;
|
|
54
54
|
text?: string;
|
package/dist/slides.js
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
import { checkedDocument, fail, same } from "./edit-helpers.js";
|
|
12
12
|
import { collectReservedPresentationIds, remapSlideTreeIds } from "./presentation-ids.js";
|
|
13
13
|
import { prepareDimensionSwitch } from "./switches.js";
|
|
14
|
+
import { mergeCatalogs } from "./catalogs.js";
|
|
14
15
|
import { checkFormat } from "./checks.js";
|
|
15
16
|
|
|
16
17
|
const isIndex = (value) => Number.isInteger(value) && value >= 0;
|
|
@@ -130,10 +131,10 @@ function newSlideId(presentation) {
|
|
|
130
131
|
}
|
|
131
132
|
|
|
132
133
|
/**
|
|
133
|
-
* Compute adding a slide. Options: `at` (the index the slide takes, default the end), `layout` (a
|
|
134
|
-
*
|
|
134
|
+
* Compute adding a slide. Options: `at` (the index the slide takes, default the end), `layout` (a layout
|
|
135
|
+
* reference, `id` or `name:id`; the layout's placeholders are added as empty slots, exactly like switching a slide's layout),
|
|
135
136
|
* `title`, `text`, `id`, `section` (default: the section of the slide before it), `slide` (a ready slide to
|
|
136
|
-
* insert, for hosts that build their own), `catalogs`/`record` for catalog lookup (as `prepareDimensionSwitch`).
|
|
137
|
+
* insert, for hosts that build their own), `catalogs`/`record`/`recordSource` for catalog lookup (as `prepareDimensionSwitch`).
|
|
137
138
|
*/
|
|
138
139
|
export function prepareAddSlide(presentation, options = {}) {
|
|
139
140
|
const slides = requireSlides(presentation);
|
|
@@ -153,7 +154,7 @@ export function prepareAddSlide(presentation, options = {}) {
|
|
|
153
154
|
// Switch the layout on a scratch copy to reuse the placeholder rules and the catalog lookup.
|
|
154
155
|
const scratch = { ...clone(presentation), slides: [...clone(slides)] };
|
|
155
156
|
scratch.slides.splice(at, 0, slide);
|
|
156
|
-
const switched = prepareDimensionSwitch(scratch, "layouts", options.layout, { slideIndex: at, catalogs: options.catalogs, record: options.record });
|
|
157
|
+
const switched = prepareDimensionSwitch(scratch, "layouts", options.layout, { slideIndex: at, catalogs: options.catalogs, record: options.record, recordSource: options.recordSource });
|
|
157
158
|
const catalogOps = switched.patches.filter((patch) => !patch.path.startsWith("/slides/"));
|
|
158
159
|
slide = switched.presentation.slides[at];
|
|
159
160
|
const patches = [...catalogOps, { op: "add", path: slidePointer(at), value: slide }];
|
|
@@ -164,7 +165,7 @@ export function prepareAddSlide(presentation, options = {}) {
|
|
|
164
165
|
/** Add a slide (see `prepareAddSlide`) as one undoable step. The change reports `index`, `id` and `selection`. */
|
|
165
166
|
export function addSlide(editor, options = {}, meta) {
|
|
166
167
|
requireEditor(editor);
|
|
167
|
-
return apply(editor, prepareAddSlide(editor.presentation, options), meta, "add");
|
|
168
|
+
return apply(editor, prepareAddSlide(editor.presentation, { ...options, catalogs: mergeCatalogs(editor.catalogs, options.catalogs) }), meta, "add");
|
|
168
169
|
}
|
|
169
170
|
|
|
170
171
|
// --- duplicate -------------------------------------------------------------------------------------
|
package/dist/svelte.js
CHANGED
|
@@ -50,8 +50,8 @@ export function opfCatalogSelect(node, params) {
|
|
|
50
50
|
node.setAttribute?.("data-opf-catalog-kind", catalogKind);
|
|
51
51
|
if (label) node.setAttribute?.("aria-label", label);
|
|
52
52
|
replaceOptions(node, getCatalogOptions(catalogKind, {
|
|
53
|
+
editor,
|
|
53
54
|
catalogs: nextParams.catalogs,
|
|
54
|
-
catalogSources: nextParams.catalogSources,
|
|
55
55
|
presentation: nextParams.presentation ?? editor.presentation
|
|
56
56
|
}));
|
|
57
57
|
|
|
@@ -61,7 +61,6 @@ export function opfCatalogSelect(node, params) {
|
|
|
61
61
|
};
|
|
62
62
|
const onChange = () => setCatalogId(editor, path, catalogKind, node.value, {
|
|
63
63
|
catalogs: nextParams.catalogs,
|
|
64
|
-
catalogSources: nextParams.catalogSources,
|
|
65
64
|
presentation: nextParams.presentation,
|
|
66
65
|
source: "svelte-catalog-select"
|
|
67
66
|
});
|