@openpresentation/opf-pptx 0.14.0 → 0.16.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 +29 -26
- package/dist/background-import.js +33 -17
- package/dist/background.js +28 -25
- package/dist/chart-types.js +7 -22
- package/dist/data-provenance.js +68 -11
- package/dist/document-provenance.js +235 -132
- package/dist/{chart-alt.js → frame-alt.js} +4 -4
- package/dist/image-fallback-browser.js +5 -0
- package/dist/image-fallback-node.js +20 -2
- package/dist/image-provenance.js +273 -0
- package/dist/index.d.ts +39 -20
- package/dist/index.js +333 -196
- package/dist/script-fonts.js +41 -60
- package/dist/theme-colors.js +15 -10
- package/dist/typeface-inventory.js +1 -1
- package/package.json +15 -4
- package/dist/slide-image-provenance.js +0 -177
|
@@ -2,8 +2,8 @@ import {XMLParser} from 'fast-xml-parser';
|
|
|
2
2
|
import {decodeTextTag, encodeTextTag} from './code-provenance.js';
|
|
3
3
|
import {contentTopology, rebuildContent, validateTopology, listLineBreaks, validateListBreaks} from './content-topology.js';
|
|
4
4
|
import {runColorRecord, validateRunColors} from './run-colors.js';
|
|
5
|
-
import {
|
|
6
|
-
import {resolveSocialProfile} from '@openpresentation/opf/composition';
|
|
5
|
+
import {validateCatalogRecord} from '@openpresentation/opf';
|
|
6
|
+
import {parseReference, resolveReference, resolveSocialProfile} from '@openpresentation/opf/composition';
|
|
7
7
|
|
|
8
8
|
// FF-32: document and slide references survive a PPTX round trip.
|
|
9
9
|
//
|
|
@@ -25,11 +25,24 @@ import {resolveSocialProfile} from '@openpresentation/opf/composition';
|
|
|
25
25
|
// size-limited, validated with the whole imported document, and never executed.
|
|
26
26
|
//
|
|
27
27
|
// Layout intent (FF-29) is part of each OPF_SLIDE_V1 record: the slide's
|
|
28
|
-
// layout
|
|
29
|
-
// the
|
|
30
|
-
//
|
|
31
|
-
//
|
|
32
|
-
//
|
|
28
|
+
// layout reference, type, composition and composition hints, plus
|
|
29
|
+
// `layoutRecord`, the record the document embeds for that reference:
|
|
30
|
+
// `{group, id, source?, record}` (OPF 0.15, FA-23), where `group` is the
|
|
31
|
+
// catalogs group the reference resolved in (`custom`, `default` or a named
|
|
32
|
+
// group), `source` that group's source and `record` the embedded record. The
|
|
33
|
+
// record is kept on the slide as well as in the document's catalogs, so a
|
|
34
|
+
// slide keeps its layout when OPF_DOCUMENT_V1 is missing or unreadable (for
|
|
35
|
+
// example a slide pasted into another deck), and a pasted slide brings its own
|
|
36
|
+
// record and the group it belongs to. A layout that resolves only in a host's
|
|
37
|
+
// registered catalog is not embedded by the export and stores no record.
|
|
38
|
+
//
|
|
39
|
+
// OPF_DOCUMENT_V1.catalogs (FA-23) is the document's grouped catalogs
|
|
40
|
+
// (`catalogs.<group>.<kind>.<id>`), pruned to the records the stored values
|
|
41
|
+
// reference (a reference inside a theme resolves in the theme's group first);
|
|
42
|
+
// every group declaration (`source`, `default: false`) is kept, since a
|
|
43
|
+
// `name:id` reference is only valid while its group is declared. The 0.14
|
|
44
|
+
// shapes (`catalogs.<kind>.records`, a layoutRecord carrying its own `id`) are
|
|
45
|
+
// not read: a tag written by an older exporter restores without them.
|
|
33
46
|
//
|
|
34
47
|
// Spec-gap closure P1 adds, in `full` mode: the document `filename` and
|
|
35
48
|
// `extensions`, the whole `assets` registry, `design.logo` (BRAND_ASSETS, also
|
|
@@ -60,12 +73,12 @@ const own = (value, key) => object(value) && Object.hasOwn(value, key) && value[
|
|
|
60
73
|
// Furniture socials re-import as the displayed profile URL (FF-34). While a line
|
|
61
74
|
// still shows exactly what the stored authored value formats to, keep the
|
|
62
75
|
// authored form (a handle stays a handle); an edited line keeps its new URL.
|
|
63
|
-
function authoredSocials(stored, observed
|
|
76
|
+
function authoredSocials(stored, observed) {
|
|
64
77
|
if (!object(stored) || !object(observed)) return observed;
|
|
65
78
|
return Object.fromEntries(Object.entries(observed).map(([platform, value]) => {
|
|
66
79
|
const authored = stored[platform];
|
|
67
80
|
if (typeof authored !== 'string') return [platform, value];
|
|
68
|
-
const profile = resolveSocialProfile(platform, authored,
|
|
81
|
+
const profile = resolveSocialProfile(platform, authored, 'organization');
|
|
69
82
|
const shown = profile.href && !/^[a-z][a-z0-9+.-]*:/i.test(profile.text) ? `https://${profile.text}` : profile.text;
|
|
70
83
|
return [platform, shown === value ? authored : value];
|
|
71
84
|
}));
|
|
@@ -74,7 +87,7 @@ function authoredSocials(stored, observed, records) {
|
|
|
74
87
|
// Deck-level fields. References are gated by native evidence; metadata has no
|
|
75
88
|
// native PowerPoint counterpart and round-trips from the stored value.
|
|
76
89
|
export const DESIGN_REFERENCES = Object.freeze(['theme', 'colorScheme', 'fontScheme', 'dimensions', 'background']);
|
|
77
|
-
export const COMPOSITION_HINTS = Object.freeze(['titleAlignment', 'contentAlignment', 'contentBox', 'contentDirection', 'chartPrimary', '
|
|
90
|
+
export const COMPOSITION_HINTS = Object.freeze(['titleAlignment', 'contentAlignment', 'contentBox', 'contentDirection', 'chartPrimary', 'imageFit', 'listBullet']);
|
|
78
91
|
// `references` (RR-34): the deck's cited sources. Like `author`, stored as a top-level key of OPF_DOCUMENT_V1 (importers up to
|
|
79
92
|
// 0.11.9 drop a tag with an unknown `supplement` field but ignore an unknown top-level key) and read back into metadata.
|
|
80
93
|
export const METADATA = Object.freeze(['narrative', 'tone', 'audience', 'purpose', 'language', 'organization', 'speaker', 'takeaway', 'duration', 'tags', 'variables', 'filename', 'extensions', 'author', 'references']);
|
|
@@ -112,6 +125,16 @@ const SLIDE_DESIGN_FIELDS = [...STYLE_REFERENCES, 'background', ...COMPOSITION_H
|
|
|
112
125
|
// every new design or metadata key here, never to the legacy sections.
|
|
113
126
|
const DOCUMENT_SUPPLEMENT = Object.freeze({design: BRAND_ASSETS, metadata: Object.freeze(['filename', 'extensions'])});
|
|
114
127
|
const SLIDE_SUPPLEMENT = Object.freeze({design: BRAND_ASSETS});
|
|
128
|
+
// RR-59: document-level values the import reads back although export only baked them in from engine defaults: the
|
|
129
|
+
// canonical `$schema`, the package title (`name`), the language the runs carry, and the theme, colour scheme, font scheme
|
|
130
|
+
// and slide size of the generated theme. In `full` mode OPF_DOCUMENT_V1.absent lists the ones the source did not state
|
|
131
|
+
// (a top-level key, which every published importer ignores), and import leaves each of them absent while the native
|
|
132
|
+
// value it would be read from is still the default the export wrote; a value edited in PowerPoint is imported as observed.
|
|
133
|
+
// (`author`, the deck background and the slides inheriting it already return absent whenever the document tag exists.)
|
|
134
|
+
export const DEFAULTED = Object.freeze(['$schema', 'name', 'language', 'design.theme', 'design.colorScheme', 'design.fontScheme', 'design.dimensions']);
|
|
135
|
+
// The package title export writes when the document has no `name` (and no `filename`).
|
|
136
|
+
export const DEFAULT_TITLE = 'OPF Presentation';
|
|
137
|
+
const statedAt = (presentation, path) => path.startsWith('design.') ? own(presentation.design, path.slice(7)) : own(presentation, path);
|
|
115
138
|
|
|
116
139
|
// Storage shape: move the supplement keys out of the legacy sections.
|
|
117
140
|
function toStoredShape(record, spec) {
|
|
@@ -294,9 +317,15 @@ function nativeSlide(entries, path) {
|
|
|
294
317
|
const root = parser.parse(dec.decode(entries[path]))?.['p:sld'];
|
|
295
318
|
const cSld = root?.['p:cSld'], tree = cSld?.['p:spTree'];
|
|
296
319
|
const rels = relationships(entries, path);
|
|
320
|
+
// FA-23: an image background with alt text is a back picture and its overlay is a shape (src/image-provenance.js); both
|
|
321
|
+
// are evidence of design.background with the native slide background, so moving or recolouring them is a background edit.
|
|
322
|
+
const named = (nodes, key) => array(nodes).filter(node => /^OPF background /.test(String(node?.[key]?.['p:cNvPr']?.name ?? '')));
|
|
323
|
+
const backgroundObjects = [...named(tree?.['p:pic'], 'p:nvPicPr').map(node => ({spPr: node['p:spPr'], blipFill: node['p:blipFill']})),
|
|
324
|
+
...named(tree?.['p:sp'], 'p:nvSpPr').map(node => ({spPr: node['p:spPr']}))];
|
|
325
|
+
const background = backgroundObjects.length ? {bg: cSld?.['p:bg'] ?? null, objects: backgroundObjects} : cSld?.['p:bg'] ?? null;
|
|
297
326
|
return {
|
|
298
327
|
structure: hash(structure(tree)),
|
|
299
|
-
background: hash(canonical(withResolvedRelationships(withoutExtensions(
|
|
328
|
+
background: hash(canonical(withResolvedRelationships(withoutExtensions(background), rels, entries))),
|
|
300
329
|
style: hash(styleSignature(withoutExtensions(tree ?? null)))
|
|
301
330
|
};
|
|
302
331
|
}
|
|
@@ -309,17 +338,18 @@ const sizeOf = value => enc.encode(JSON.stringify(value)).byteLength;
|
|
|
309
338
|
// The tag value is the UTF-8 JSON as uppercase hex: two characters per byte.
|
|
310
339
|
const tagChars = value => sizeOf(value) * 2;
|
|
311
340
|
const SOURCE_KEYS = new Set(['src', 'image', 'logo', 'photo']);
|
|
312
|
-
|
|
313
|
-
const
|
|
341
|
+
// An image source: an asset id, a URL, a data URI or a path relative to the document (the 0.15 image source forms).
|
|
342
|
+
const SOURCE_PREFIX = /^(asset:|data:|https?:|file:|\.\.?\/)/i;
|
|
343
|
+
// Metadata fields that hold content references (OPF 0.15: `id` or `name:id`; for audience and purpose a string that is not
|
|
344
|
+
// a reference is free text). `language` is an engine vocabulary (a BCP-47 tag), not a catalog reference.
|
|
345
|
+
const METADATA_REFERENCES = Object.freeze(['narrative', 'tone', 'purpose', 'audience']);
|
|
346
|
+
const LAYOUT_SCHEMA = 'https://openpresentation.org/schema/opf-layout/v1';
|
|
347
|
+
const GROUP_NAME = /^[a-z][a-z0-9-]*$/;
|
|
314
348
|
|
|
315
349
|
function collectStrings(value, into = new Set()) {
|
|
316
350
|
if (typeof value === 'string') into.add(value);
|
|
317
351
|
else if (Array.isArray(value)) value.forEach(item => collectStrings(item, into));
|
|
318
|
-
else if (object(value)) for (const
|
|
319
|
-
// Socials keys are socialPlatforms catalog references (FF-34).
|
|
320
|
-
if (key === 'socials' && object(item)) Object.keys(item).forEach(id => into.add(id));
|
|
321
|
-
collectStrings(item, into);
|
|
322
|
-
}
|
|
352
|
+
else if (object(value)) for (const item of Object.values(value)) collectStrings(item, into);
|
|
323
353
|
return into;
|
|
324
354
|
}
|
|
325
355
|
|
|
@@ -331,66 +361,93 @@ function carriesSource(value) {
|
|
|
331
361
|
return false;
|
|
332
362
|
}
|
|
333
363
|
|
|
334
|
-
//
|
|
335
|
-
|
|
336
|
-
|
|
364
|
+
// A reference's id: a string reference, or the `id` of an object reference (`{id, ...overrides}`).
|
|
365
|
+
const referenceOf = value => typeof value === 'string' ? value : object(value) && typeof value.id === 'string' ? value.id : undefined;
|
|
366
|
+
|
|
367
|
+
// The content references a document (or the stored subset of one) writes, as core resolves them: the root narrative,
|
|
368
|
+
// audience, purpose and tone, a language object's font schemes, the deck and slide designs and each slide's layout.
|
|
369
|
+
function documentReferences(doc) {
|
|
370
|
+
const references = [];
|
|
371
|
+
const push = (kind, value) => {
|
|
372
|
+
const reference = referenceOf(value);
|
|
373
|
+
if (reference !== undefined && parseReference(reference)) references.push({kind, reference});
|
|
374
|
+
};
|
|
375
|
+
const design = value => {
|
|
376
|
+
if (!object(value)) return;
|
|
377
|
+
push('themes', value.theme);
|
|
378
|
+
push('colorSchemes', value.colorScheme);
|
|
379
|
+
push('fontSchemes', value.fontScheme);
|
|
380
|
+
};
|
|
381
|
+
push('narratives', doc.narrative);
|
|
382
|
+
for (const audience of array(doc.audience)) push('audiences', audience);
|
|
383
|
+
push('purposes', doc.purpose);
|
|
384
|
+
push('tones', doc.tone);
|
|
385
|
+
if (object(doc.language)) { push('fontSchemes', doc.language.fontScheme); push('fontSchemes', doc.language.googleFontScheme); }
|
|
386
|
+
design(doc.design);
|
|
387
|
+
for (const slide of array(doc.slides)) if (object(slide)) { push('layouts', slide.layout); design(slide.design); }
|
|
388
|
+
return references;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// The embedded catalogs a set of references needs: each referenced record once, in the group it resolves in, plus the
|
|
392
|
+
// records it references (a theme's colour and font schemes, resolved in the theme's group first), and every group
|
|
393
|
+
// declaration (its `source`, or `default: false`). Only embedded records are kept; nothing resolves in a host catalog here.
|
|
394
|
+
export function referencedCatalogs(catalogs, references) {
|
|
337
395
|
if (!object(catalogs)) return undefined;
|
|
396
|
+
const document = {catalogs};
|
|
338
397
|
const result = {};
|
|
339
|
-
for (const [
|
|
340
|
-
if (
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
398
|
+
for (const [name, group] of Object.entries(catalogs)) {
|
|
399
|
+
if (name === 'default' && group === false) result[name] = false;
|
|
400
|
+
else if (object(group) && typeof group.source === 'string') result[name] = {source: group.source};
|
|
401
|
+
}
|
|
402
|
+
const queue = [...references], seen = new Set();
|
|
403
|
+
while (queue.length) {
|
|
404
|
+
const {kind, reference, group} = queue.shift();
|
|
405
|
+
const key = `${group ?? ''}|${kind}|${reference}`;
|
|
406
|
+
if (seen.has(key)) continue;
|
|
407
|
+
seen.add(key);
|
|
408
|
+
const found = resolveReference(document, kind, reference, {catalogs: [], ...(group !== undefined ? {group} : {})});
|
|
409
|
+
if (!found || found.origin !== 'document') continue;
|
|
410
|
+
const holder = result[found.group] = object(result[found.group]) ? result[found.group] : {};
|
|
411
|
+
holder[kind] = {...(object(holder[kind]) ? holder[kind] : {}), [found.id]: clone(found.record)};
|
|
412
|
+
if (kind === 'themes') for (const [schemeKind, field] of [['colorSchemes', 'colorScheme'], ['fontSchemes', 'fontScheme']]) {
|
|
413
|
+
const inner = referenceOf(found.record[field]);
|
|
414
|
+
if (inner !== undefined && parseReference(inner)) queue.push({kind: schemeKind, reference: inner, group: found.group});
|
|
347
415
|
}
|
|
348
416
|
}
|
|
349
417
|
return Object.keys(result).length ? result : undefined;
|
|
350
418
|
}
|
|
351
419
|
|
|
352
|
-
// A referenced record can itself refer to another inline record (a theme to its color scheme).
|
|
353
|
-
function referencedCatalogs(catalogs, referenced) {
|
|
354
|
-
let pruned = pruneCatalogs(catalogs, referenced);
|
|
355
|
-
for (let pass = 0; pass < 3 && pruned; pass += 1) {
|
|
356
|
-
const ids = new Set(referenced);
|
|
357
|
-
for (const entry of Object.values(pruned)) for (const record of array(Array.isArray(entry) ? entry : entry?.records)) collectStrings(record, ids);
|
|
358
|
-
const next = pruneCatalogs(catalogs, ids);
|
|
359
|
-
if (same(next, pruned)) break;
|
|
360
|
-
pruned = next;
|
|
361
|
-
}
|
|
362
|
-
return pruned;
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
const layoutRecords = catalogs => array(Array.isArray(catalogs?.layouts) ? catalogs.layouts : catalogs?.layouts?.records);
|
|
366
|
-
|
|
367
420
|
// Presentation validation deliberately accepts arbitrary inline catalog objects.
|
|
368
421
|
// Recovered layouts must also pass their companion schema before composition
|
|
369
422
|
// can use them (for example, a null placeholder otherwise crashes rendering).
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
423
|
+
// An embedded record carries no `$schema` or `id` (its key is the id): both are
|
|
424
|
+
// supplied for validation only; the authored record is never changed.
|
|
425
|
+
function validLayoutRecord(value, id) {
|
|
426
|
+
// An authored `$schema` or `id` is validated as it is (a foreign `$schema` fails).
|
|
427
|
+
try { return object(value) && validateCatalogRecord('layouts', {$schema: LAYOUT_SCHEMA, id, ...value}).valid; }
|
|
374
428
|
catch { return false; }
|
|
375
429
|
}
|
|
376
430
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
if (
|
|
384
|
-
const
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
431
|
+
// Every embedded layout record of the stored catalogs, with packaged media resolved; a record that does not validate is
|
|
432
|
+
// left out (its group stays) and reported at catalogs.<group>.layouts.<id>.
|
|
433
|
+
function validatedLayoutCatalog(catalogs, entries, report) {
|
|
434
|
+
if (!object(catalogs)) return catalogs;
|
|
435
|
+
const result = {};
|
|
436
|
+
for (const [name, group] of Object.entries(catalogs)) {
|
|
437
|
+
if (!object(group) || !object(group.layouts)) { result[name] = group; continue; }
|
|
438
|
+
const layouts = {};
|
|
439
|
+
for (const [id, record] of Object.entries(group.layouts)) {
|
|
440
|
+
const {value, unresolved} = resolveMedia(record, entries);
|
|
441
|
+
if (!unresolved && validLayoutRecord(value, id)) { layouts[id] = value; continue; }
|
|
442
|
+
const path = `catalogs.${name}.layouts.${id}`;
|
|
443
|
+
report({code: unresolved ? 'unresolved-asset-reference' : 'invalid-document-provenance', path,
|
|
444
|
+
message: `The stored layout record at ${path} ${unresolved ? 'refers to unavailable media' : 'does not validate against the layouts catalog schema'}, so it was not restored.`});
|
|
445
|
+
}
|
|
446
|
+
const kept = {...group};
|
|
447
|
+
if (Object.keys(layouts).length) kept.layouts = layouts; else delete kept.layouts;
|
|
448
|
+
result[name] = kept;
|
|
449
|
+
}
|
|
450
|
+
return result;
|
|
394
451
|
}
|
|
395
452
|
|
|
396
453
|
const assetReferences = value => [...collectStrings(value)].filter(item => item.startsWith('asset:')).map(item => item.slice(6));
|
|
@@ -409,7 +466,7 @@ const assetReferences = value => [...collectStrings(value)].filter(item => item.
|
|
|
409
466
|
* catalog records those ids need. It stores no organization, speaker,
|
|
410
467
|
* free-text metadata, slide ids or assets.
|
|
411
468
|
*/
|
|
412
|
-
export function documentProvenance(presentation, {mode = 'full',
|
|
469
|
+
export function documentProvenance(presentation, {mode = 'full', layoutOf = () => undefined, report = () => {}} = {}) {
|
|
413
470
|
const referencesOnly = mode === 'references-only';
|
|
414
471
|
const design = {}, metadata = {};
|
|
415
472
|
for (const key of DESIGN_FIELDS) {
|
|
@@ -422,21 +479,24 @@ export function documentProvenance(presentation, {mode = 'full', isCatalogId = (
|
|
|
422
479
|
const value = presentation[key];
|
|
423
480
|
if (key === 'author' && (referencesOnly || !authorNeedsRecord(value))) continue;
|
|
424
481
|
if (referencesOnly) {
|
|
425
|
-
|
|
482
|
+
// A language tag is stored as it is; a reference field only while every value is a reference (never free text).
|
|
426
483
|
const ids = typeof value === 'string' ? [value] : key === 'audience' && Array.isArray(value) && value.every(item => typeof item === 'string') ? value : null;
|
|
427
|
-
if (!
|
|
484
|
+
if (!ids || !(key === 'language' || (METADATA_REFERENCES.includes(key) && ids.every(id => parseReference(id))))) continue;
|
|
428
485
|
}
|
|
429
486
|
metadata[key] = clone(value);
|
|
430
487
|
}
|
|
431
488
|
const slides = presentation.slides.map((slide, index) => {
|
|
432
489
|
const record = {v: 1, slide: index};
|
|
433
490
|
for (const key of [...(referencesOnly ? [] : ['id', ...SLIDE_METADATA]), 'beat', ...SLIDE_STRUCTURE]) if (own(slide, key)) record[key] = clone(slide[key]);
|
|
434
|
-
|
|
491
|
+
// The record the document embeds for the slide's layout reference, with the group it resolved in: core's provenance of
|
|
492
|
+
// the slide's resolved layout (resolveSlideContext().resolved.provenance.layout), passed in by the exporter.
|
|
493
|
+
const found = typeof slide.layout === 'string' ? layoutOf(index) : undefined;
|
|
435
494
|
// The layout record is a catalog record. 'references-only' stores it only
|
|
436
495
|
// when it names no image, file or URL; neither mode stores the assets it
|
|
437
496
|
// references (as before FF-29, catalog records never pulled in assets).
|
|
438
|
-
|
|
439
|
-
|
|
497
|
+
if (found?.origin === 'document' && !(referencesOnly && carriesSource(found.record))) {
|
|
498
|
+
record.layoutRecord = {group: found.group, id: found.id, ...(found.source !== undefined ? {source: found.source} : {}), record: clone(found.record)};
|
|
499
|
+
}
|
|
440
500
|
const slideDesign = {};
|
|
441
501
|
for (const key of SLIDE_DESIGN_FIELDS) {
|
|
442
502
|
if (!own(slide.design, key) || (referencesOnly && carriesSource(slide.design[key]))) continue;
|
|
@@ -446,17 +506,22 @@ export function documentProvenance(presentation, {mode = 'full', isCatalogId = (
|
|
|
446
506
|
return record;
|
|
447
507
|
});
|
|
448
508
|
const document = {v: 1, slides: slides.length};
|
|
509
|
+
// 'references-only' stores no absence: it leaves stated values out, so a missing key there proves nothing.
|
|
510
|
+
if (!referencesOnly) {
|
|
511
|
+
const absent = DEFAULTED.filter(path => !statedAt(presentation, path));
|
|
512
|
+
if (absent.length) document.absent = absent;
|
|
513
|
+
}
|
|
449
514
|
if (Object.keys(design).length) document.design = design;
|
|
450
515
|
if (Object.keys(metadata).length) document.metadata = metadata;
|
|
451
|
-
const stored = [design, metadata, slides];
|
|
452
516
|
// 'full' stores the whole registry, so unreferenced assets return as authored;
|
|
453
517
|
// 'references-only' stores no assets at all.
|
|
454
518
|
if (!referencesOnly && object(presentation.assets) && Object.keys(presentation.assets).length) document.assets = clone(presentation.assets);
|
|
455
|
-
|
|
456
|
-
const catalogRecords = referencedCatalogs(catalogs,
|
|
519
|
+
// The embedded records (and group declarations) the stored values reference.
|
|
520
|
+
const catalogRecords = referencedCatalogs(presentation.catalogs, documentReferences({...metadata, design, slides}));
|
|
457
521
|
if (catalogRecords) document.catalogs = catalogRecords;
|
|
458
|
-
// Slide content topology is added per slide by the exporter (recordContentTopology).
|
|
459
|
-
|
|
522
|
+
// Slide content topology is added per slide by the exporter (recordContentTopology). In 'full' mode every document states
|
|
523
|
+
// something: the defaults it leaves to the engine (`absent`) are worth the tags, so import can leave them absent.
|
|
524
|
+
const stated = !referencesOnly || Object.keys(design).length || Object.keys(metadata).length || document.assets !== undefined || slides.some(record => Object.keys(record).length > 2);
|
|
460
525
|
return {mode, document, slides, report, stated: Boolean(stated)};
|
|
461
526
|
}
|
|
462
527
|
|
|
@@ -599,6 +664,7 @@ function storable(entries, provenance) {
|
|
|
599
664
|
};
|
|
600
665
|
const source = provenance.document;
|
|
601
666
|
const document = {v: 1, slides: source.slides};
|
|
667
|
+
if (source.absent) document.absent = [...source.absent];
|
|
602
668
|
const assets = {};
|
|
603
669
|
for (const [id, asset] of Object.entries(source.assets ?? {})) {
|
|
604
670
|
const value = prepare(`assets.${id}`, asset);
|
|
@@ -689,8 +755,9 @@ function storable(entries, provenance) {
|
|
|
689
755
|
* `entries` maps part paths to bytes and is updated in place.
|
|
690
756
|
*/
|
|
691
757
|
export function attachDocumentProvenance(entries, provenance) {
|
|
692
|
-
// A document that states nothing (
|
|
693
|
-
// content topology) gets no tags and its bytes are
|
|
758
|
+
// A document that states nothing (in 'references-only' mode: no references,
|
|
759
|
+
// metadata, slide fields or content topology) gets no tags and its bytes are
|
|
760
|
+
// unchanged. In 'full' mode the defaults it leaves absent are always recorded.
|
|
694
761
|
if (!provenance || !(provenance.stated || provenance.slides.some(record => record.content !== undefined))) return;
|
|
695
762
|
const presentationXml = dec.decode(entries['ppt/presentation.xml']);
|
|
696
763
|
const presentationRoot = parser.parse(presentationXml)['p:presentation'];
|
|
@@ -780,6 +847,9 @@ function validateDocument(stored) {
|
|
|
780
847
|
const references = stored.references;
|
|
781
848
|
if (references !== undefined && (!Array.isArray(references) || references.length > 4096 || !references.every(object))) throw Error('Invalid references record.');
|
|
782
849
|
validateOmitted(value.omitted);
|
|
850
|
+
// Untrusted: only a short list of strings is read, and a path this importer does not know is ignored (a newer exporter's).
|
|
851
|
+
if (value.absent !== undefined && (!Array.isArray(value.absent) || value.absent.length > 64 || !value.absent.every(path => typeof path === 'string' && path.length <= 256))) throw Error('Invalid absent field list.');
|
|
852
|
+
if (value.absent !== undefined) value.absent = value.absent.filter(path => DEFAULTED.includes(path));
|
|
783
853
|
if (author !== undefined || references !== undefined) {
|
|
784
854
|
const {author: _author, references: _references, ...rest} = value;
|
|
785
855
|
return {...rest, metadata: {...rest.metadata, ...(author !== undefined ? {author} : {}), ...(references !== undefined ? {references} : {})}};
|
|
@@ -794,7 +864,9 @@ function validateSlide(stored) {
|
|
|
794
864
|
for (const key of SLIDE_SUPPLEMENT.design) if (object(stored.design) && stored.design[key] !== undefined) throw Error(`Supplement field design.${key} stored in the legacy section.`);
|
|
795
865
|
const value = fromStoredShape(stored, SLIDE_SUPPLEMENT);
|
|
796
866
|
for (const key of Object.keys(value.design ?? {})) if (!SLIDE_DESIGN_FIELDS.includes(key)) throw Error(`Unknown slide design field ${key}.`);
|
|
797
|
-
|
|
867
|
+
const layoutRecord = value.layoutRecord;
|
|
868
|
+
if (layoutRecord !== undefined && (!object(layoutRecord) || !GROUP_NAME.test(layoutRecord.group ?? '') || !GROUP_NAME.test(layoutRecord.id ?? '') || !object(layoutRecord.record)
|
|
869
|
+
|| (layoutRecord.source !== undefined && typeof layoutRecord.source !== 'string') || Object.keys(layoutRecord).some(key => !['group', 'id', 'source', 'record'].includes(key)))) throw Error('Invalid slide layout record.');
|
|
798
870
|
if (value.section !== undefined && typeof value.section !== 'string') throw Error('Invalid slide section record.');
|
|
799
871
|
if (value.extensions !== undefined && !object(value.extensions)) throw Error('Invalid slide extensions record.');
|
|
800
872
|
if (value.content !== undefined) validateTopology(value.content);
|
|
@@ -865,14 +937,19 @@ export function slideListBreaks(entries, path, root, rels) {
|
|
|
865
937
|
* slides[i] = {layout, structure: 'match' | 'changed' | 'untagged', record, catalogRecord}
|
|
866
938
|
* is the contract for layout-structure recovery (FF-29): `record` is the
|
|
867
939
|
* validated OPF_SLIDE_V1 value (layout, type, composition, design hints, ...),
|
|
868
|
-
* `catalogRecord` the stored
|
|
940
|
+
* `catalogRecord` the stored embedded layout record for `layout`, if any
|
|
941
|
+
* (none when the layout resolves only in a registered host catalog).
|
|
942
|
+
*
|
|
943
|
+
* `catalogs` are the host's registered catalogs (core Catalog[]): a stored
|
|
944
|
+
* layout reference that neither the stored document catalogs nor a slide's
|
|
945
|
+
* own record resolves may still resolve there.
|
|
869
946
|
*
|
|
870
947
|
* `slides[i].contentBounds` are the native bounds (reference px) of the
|
|
871
948
|
* imported blocks of slide i, in block order, for content topology matching.
|
|
872
949
|
* `nativeSections` is the package's native section list as one name (or
|
|
873
950
|
* undefined) per slide, or null when the package has no list.
|
|
874
951
|
*/
|
|
875
|
-
export function restoreDocumentProvenance(imported, {entries, presentationRoot, presentationRels, slides, organizationConflict = false, speakerConflict = false,
|
|
952
|
+
export function restoreDocumentProvenance(imported, {entries, presentationRoot, presentationRels, slides, organizationConflict = false, speakerConflict = false, catalogs: hostCatalogs, nativeSections = null, schemaOption = false}, report) {
|
|
876
953
|
const invalid = message => report({code: 'invalid-document-provenance', path: '', message: `${message} Ordinary import keeps the values observed in the PPTX.`});
|
|
877
954
|
let document;
|
|
878
955
|
try {
|
|
@@ -881,8 +958,7 @@ export function restoreDocumentProvenance(imported, {entries, presentationRoot,
|
|
|
881
958
|
else document = remapMedia(validateDocument(found.value), entries);
|
|
882
959
|
} catch (error) { invalid(`${error.message}`); document = undefined; }
|
|
883
960
|
|
|
884
|
-
|
|
885
|
-
if (document) document = {...document, catalogs: validatedLayoutCatalog(document.catalogs, entries, report, rejectedLayoutIds)};
|
|
961
|
+
if (document && document.catalogs !== undefined) document = {...document, catalogs: validatedLayoutCatalog(document.catalogs, entries, report)};
|
|
886
962
|
|
|
887
963
|
const slideRecords = slides.map(({root, relationships: rels, path}, index) => {
|
|
888
964
|
try {
|
|
@@ -898,7 +974,7 @@ export function restoreDocumentProvenance(imported, {entries, presentationRoot,
|
|
|
898
974
|
const group = (field, ops, contentPaths) => groups.push({field, ops, ...(contentPaths ? {contentPaths} : {})});
|
|
899
975
|
const set = (path, value) => ({path, value});
|
|
900
976
|
const remove = path => ({path, remove: true});
|
|
901
|
-
const intent = layoutIntent(document
|
|
977
|
+
const intent = layoutIntent(document?.catalogs, slideRecords, entries, group, report, hostCatalogs);
|
|
902
978
|
|
|
903
979
|
// Slide and block ids must stay unique across the document; a duplicated
|
|
904
980
|
// slide carries copies of both, and the first occurrence keeps them.
|
|
@@ -1024,6 +1100,21 @@ export function restoreDocumentProvenance(imported, {entries, presentationRoot,
|
|
|
1024
1100
|
group(`design.${key}`, [set(['design', key], value)]);
|
|
1025
1101
|
}
|
|
1026
1102
|
const styleIntact = STYLE_REFERENCES.every(key => restoredStyle[key] !== false);
|
|
1103
|
+
// RR-59: a default the source left absent stays absent while the native value it is read from is the one export wrote
|
|
1104
|
+
// (FF-24's theme recovery, for example, gives a theme-only deck its colour scheme). A native edit is imported as observed.
|
|
1105
|
+
const absent = new Set(array(document.absent));
|
|
1106
|
+
const defaultGates = {theme: match.colors && match.fonts, colorScheme: match.colors, fontScheme: match.fonts, dimensions: match.size};
|
|
1107
|
+
for (const [key, unchanged] of Object.entries(defaultGates)) {
|
|
1108
|
+
if (absent.has(`design.${key}`) && unchanged && storedDesign[key] === undefined && own(imported.design, key)) group(`design.${key}`, [remove(['design', key])]);
|
|
1109
|
+
}
|
|
1110
|
+
// No native field holds `$schema`; an importer option that names one keeps it.
|
|
1111
|
+
if (absent.has('$schema') && !schemaOption && own(imported, '$schema')) group('$schema', [remove(['$schema'])]);
|
|
1112
|
+
// The package title export wrote for a document with no name: its filename, else the default title.
|
|
1113
|
+
const defaultTitle = own(document.metadata, 'filename') ? document.metadata.filename : array(document.omitted).includes('filename') ? undefined : DEFAULT_TITLE;
|
|
1114
|
+
if (absent.has('name') && defaultTitle !== undefined && imported.name === defaultTitle) group('name', [remove(['name'])]);
|
|
1115
|
+
// The runs' language is compared with the engine default in reconcileLanguage (src/script-fonts.js), which keeps this
|
|
1116
|
+
// removal only while the runs still carry that default.
|
|
1117
|
+
if (absent.has('language') && own(imported, 'language')) group('language', [remove(['language'])]);
|
|
1027
1118
|
// Brand images have no native gate (the P2 logo picture is consumed separately).
|
|
1028
1119
|
for (const key of BRAND_ASSETS) {
|
|
1029
1120
|
if (storedDesign[key] === undefined) continue;
|
|
@@ -1123,10 +1214,8 @@ export function restoreDocumentProvenance(imported, {entries, presentationRoot,
|
|
|
1123
1214
|
if (value === undefined) continue;
|
|
1124
1215
|
if (key === 'organization' && object(imported.organization)) {
|
|
1125
1216
|
const observed = imported.organization;
|
|
1126
|
-
//
|
|
1127
|
-
const
|
|
1128
|
-
const records = [...array(document.catalogs?.socialPlatforms?.records ?? document.catalogs?.socialPlatforms), ...hostRecords].filter(object);
|
|
1129
|
-
const merge = (stored, current) => object(current.socials) && object(stored?.socials) ? {...current, socials: authoredSocials(stored.socials, current.socials, records)} : current;
|
|
1217
|
+
// Core formats socials from its own SOCIAL_PLATFORMS vocabulary, exactly as export did.
|
|
1218
|
+
const merge = (stored, current) => object(current.socials) && object(stored?.socials) ? {...current, socials: authoredSocials(stored.socials, current.socials)} : current;
|
|
1130
1219
|
const list = array(value);
|
|
1131
1220
|
const index = list.findIndex(item => object(item) && item.id === observed.id);
|
|
1132
1221
|
if (index < 0) value = clone(observed);
|
|
@@ -1148,13 +1237,13 @@ export function restoreDocumentProvenance(imported, {entries, presentationRoot,
|
|
|
1148
1237
|
// (a media asset read from a video placeholder) keeps its observed value.
|
|
1149
1238
|
for (const [id, asset] of Object.entries(storedAssets)) if (!own(imported.assets, id)) group(`assets.${id}`, [set(['assets', id], clone(asset))]);
|
|
1150
1239
|
|
|
1151
|
-
// Assets and
|
|
1240
|
+
// Assets and embedded catalog records follow what the restored document references.
|
|
1152
1241
|
const finalize = doc => {
|
|
1153
1242
|
const needed = [...new Set(assetReferences(doc))].filter(id => !own(doc.assets, id) && Object.hasOwn(storedAssets, id));
|
|
1154
1243
|
if (needed.length) doc.assets = {...(object(doc.assets) ? doc.assets : {}), ...Object.fromEntries(needed.map(id => [id, clone(storedAssets[id])]))};
|
|
1155
1244
|
if (object(document.catalogs)) {
|
|
1156
1245
|
const {catalogs: _ignored, ...rest} = doc;
|
|
1157
|
-
const {value, unresolved} = resolveMedia(referencedCatalogs(document.catalogs,
|
|
1246
|
+
const {value, unresolved} = resolveMedia(referencedCatalogs(document.catalogs, documentReferences(rest)), entries);
|
|
1158
1247
|
if (value && !unresolved) doc.catalogs = value;
|
|
1159
1248
|
}
|
|
1160
1249
|
return intent.finalize(doc);
|
|
@@ -1163,74 +1252,79 @@ export function restoreDocumentProvenance(imported, {entries, presentationRoot,
|
|
|
1163
1252
|
}
|
|
1164
1253
|
|
|
1165
1254
|
/**
|
|
1166
|
-
* Layout intent of the tagged slides (FF-29): layout
|
|
1167
|
-
* composition hints are restored while a slide's arrangement
|
|
1168
|
-
* Each layout
|
|
1169
|
-
*
|
|
1170
|
-
* -
|
|
1171
|
-
*
|
|
1172
|
-
*
|
|
1173
|
-
*
|
|
1174
|
-
*
|
|
1255
|
+
* Layout intent of the tagged slides (FF-29): layout reference, type,
|
|
1256
|
+
* composition and composition hints are restored while a slide's arrangement
|
|
1257
|
+
* is unchanged. Each layout reference resolves to exactly one record, in the
|
|
1258
|
+
* order core resolves references (embedded records before registered ones):
|
|
1259
|
+
* - the record the stored document catalogs embed (OPF_DOCUMENT_V1);
|
|
1260
|
+
* - a slide's own `layoutRecord`, embedded under its group by `finalize` (a
|
|
1261
|
+
* slide whose group the stored document declares with another source cannot
|
|
1262
|
+
* carry its record there). When the reference also resolves in a registered
|
|
1263
|
+
* catalog, a slide's record is used only when every restored slide with that
|
|
1264
|
+
* reference carries the same one, so it never changes another slide's layout;
|
|
1265
|
+
* - a record of the host's registered catalogs (`hostCatalogs`), which the
|
|
1266
|
+
* imported document then references without embedding it.
|
|
1175
1267
|
* A slide whose own record disagrees with the chosen one keeps its content
|
|
1176
|
-
* without the layout
|
|
1177
|
-
*
|
|
1178
|
-
* imported document always renders.
|
|
1179
|
-
* that the imported document references.
|
|
1268
|
+
* without the layout reference (`layout-reference-changed`), and a reference
|
|
1269
|
+
* that resolves nowhere is not restored (`unresolved-reference`), so
|
|
1270
|
+
* the imported document always renders.
|
|
1180
1271
|
*/
|
|
1181
|
-
function layoutIntent(
|
|
1182
|
-
const
|
|
1272
|
+
export function layoutIntent(storedCatalogs, slideRecords, entries, group, report, hostCatalogs) {
|
|
1273
|
+
const stored = {catalogs: object(storedCatalogs) ? storedCatalogs : {}};
|
|
1274
|
+
const declared = name => Object.hasOwn(stored.catalogs, name) ? stored.catalogs[name] : undefined;
|
|
1183
1275
|
const owns = slideRecords.map(entry => {
|
|
1184
1276
|
if (!entry) return null;
|
|
1185
1277
|
const {record, native: observed} = entry;
|
|
1186
1278
|
const structureMatch = record.native.structure === observed.structure;
|
|
1187
1279
|
const layout = typeof record.layout === 'string' ? record.layout : undefined;
|
|
1280
|
+
const parsed = layout === undefined ? undefined : parseReference(layout);
|
|
1188
1281
|
let own = record.layoutRecord, problem;
|
|
1189
|
-
// A record for another id is ignored and reported.
|
|
1190
|
-
|
|
1282
|
+
// A record for another reference (another id, or a group the reference does not name) is ignored and reported.
|
|
1283
|
+
const matches = own !== undefined && parsed !== undefined && own.id === parsed.id
|
|
1284
|
+
&& (parsed.group !== undefined ? own.group === parsed.group : own.group === 'custom' || own.group === 'default');
|
|
1285
|
+
if (own !== undefined && !matches) { problem = layout === undefined ? undefined : 'mismatch'; own = undefined; }
|
|
1191
1286
|
else if (own !== undefined) {
|
|
1192
|
-
const {value, unresolved} = resolveMedia(own, entries);
|
|
1287
|
+
const {value, unresolved} = resolveMedia(own.record, entries);
|
|
1288
|
+
const target = declared(own.group);
|
|
1193
1289
|
if (unresolved) { own = undefined; problem = 'media'; }
|
|
1194
|
-
else if (!validLayoutRecord(value)) { own = undefined; problem = 'schema'; }
|
|
1195
|
-
else own =
|
|
1290
|
+
else if (!validLayoutRecord(value, own.id)) { own = undefined; problem = 'schema'; }
|
|
1291
|
+
else if (own.group !== 'custom' && target !== undefined && (target === false || !object(target) || target.source !== own.source)) { own = undefined; problem = 'source'; }
|
|
1292
|
+
else own = {...own, record: value};
|
|
1196
1293
|
}
|
|
1197
1294
|
return {structureMatch, layout, own, problem, stated: record.layoutRecord};
|
|
1198
1295
|
});
|
|
1199
1296
|
const resolved = new Map();
|
|
1200
|
-
for (const
|
|
1201
|
-
const members = owns.filter(item => item?.structureMatch && item.layout ===
|
|
1202
|
-
const fromDocument =
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
const first = members.find(item => item.own);
|
|
1210
|
-
if (first) resolved.set(id, {record: first.own, source: 'slides', add: true});
|
|
1211
|
-
}
|
|
1297
|
+
for (const layout of new Set(owns.filter(item => item?.structureMatch && item.layout !== undefined).map(item => item.layout))) {
|
|
1298
|
+
const members = owns.filter(item => item?.structureMatch && item.layout === layout);
|
|
1299
|
+
const fromDocument = resolveReference(stored, 'layouts', layout, {catalogs: []});
|
|
1300
|
+
const first = members.find(item => item.own);
|
|
1301
|
+
const fromHost = fromDocument ? undefined : resolveReference(stored, 'layouts', layout, {catalogs: Array.isArray(hostCatalogs) ? hostCatalogs : []});
|
|
1302
|
+
const agreed = first && members.every(item => item.own && same(item.own.record, first.own.record));
|
|
1303
|
+
if (fromDocument) resolved.set(layout, {record: fromDocument.record, source: 'document'});
|
|
1304
|
+
else if (first && (!fromHost || agreed)) resolved.set(layout, {record: first.own.record, source: 'slides', add: first.own});
|
|
1305
|
+
else if (fromHost) resolved.set(layout, {record: fromHost.record, source: 'host'});
|
|
1212
1306
|
}
|
|
1213
|
-
const
|
|
1214
|
-
const against = {document: 'the document', bundled: 'the built-in layout', slides: 'another slide'};
|
|
1307
|
+
const against = {document: 'the document', host: 'the registered catalog', slides: 'another slide'};
|
|
1215
1308
|
// `typeInContent`: the slide stores a content topology, so its `type` is restored with that group (or not at all).
|
|
1216
1309
|
const slide = (index, {record}, typeInContent = false) => {
|
|
1217
1310
|
const {structureMatch, layout, own, problem, stated} = owns[index];
|
|
1218
1311
|
const {native: _native, ...recordValue} = record;
|
|
1219
|
-
if (structureMatch && problem === 'mismatch') report({code: 'invalid-document-provenance', path: `slides.${index}.layoutRecord`, message: `The stored layout record '${stated?.id}' does not match slides.${index}.layout '${layout}', so it was not restored.`});
|
|
1312
|
+
if (structureMatch && problem === 'mismatch') report({code: 'invalid-document-provenance', path: `slides.${index}.layoutRecord`, message: `The stored layout record '${stated?.group}:${stated?.id}' does not match slides.${index}.layout '${layout}', so it was not restored.`});
|
|
1220
1313
|
if (structureMatch && problem === 'media') report({code: 'unresolved-asset-reference', path: `slides.${index}.layoutRecord`, message: `slides.${index}.layoutRecord refers to a picture that is no longer in the PPTX, so the slide's stored layout record was not restored.`});
|
|
1221
1314
|
if (structureMatch && problem === 'schema') report({code: 'invalid-document-provenance', path: `slides.${index}.layoutRecord`, message: `slides.${index}.layoutRecord does not validate against the layouts catalog schema, so the slide's stored layout record was not restored.`});
|
|
1315
|
+
if (structureMatch && problem === 'source') report({code: 'invalid-document-provenance', path: `slides.${index}.layoutRecord`, message: `slides.${index}.layoutRecord belongs to catalogs.${stated.group}${stated.source ? ` (${stated.source})` : ''}, which the document declares with another source, so the slide's stored layout record was not restored.`});
|
|
1222
1316
|
const target = layout === undefined ? undefined : resolved.get(layout);
|
|
1223
1317
|
let restoreLayout = structureMatch;
|
|
1224
1318
|
if (structureMatch && layout !== undefined) {
|
|
1225
1319
|
if (!target) {
|
|
1226
1320
|
restoreLayout = false;
|
|
1227
|
-
report({code: 'unresolved-
|
|
1228
|
-
} else if (own && !same(own, target.record)) {
|
|
1321
|
+
report({code: 'unresolved-reference', path: `slides.${index}.layout`, message: `Layout '${layout}' resolves neither in the catalogs the PPTX stores nor in a registered catalog, so slides.${index}.layout was not restored; the imported slide keeps its observed arrangement.`});
|
|
1322
|
+
} else if (own && !same(own.record, target.record)) {
|
|
1229
1323
|
restoreLayout = false;
|
|
1230
1324
|
report({code: 'layout-reference-changed', path: `slides.${index}.layout`, message: `This slide stores a different record for layout '${layout}' than ${against[target.source]}, so slides.${index}.layout was not restored; the imported slide keeps its observed arrangement.`});
|
|
1231
1325
|
}
|
|
1232
1326
|
}
|
|
1233
|
-
const catalogRecord = target && target.source !== '
|
|
1327
|
+
const catalogRecord = target && target.source !== 'host' ? target.record : undefined;
|
|
1234
1328
|
const at = (...path) => ['slides', index, ...path];
|
|
1235
1329
|
for (const key of SLIDE_STRUCTURE) {
|
|
1236
1330
|
if (record[key] === undefined || (key === 'type' && typeInContent)) continue;
|
|
@@ -1246,11 +1340,20 @@ function layoutIntent(documentRecords, hasDocument, slideRecords, entries, group
|
|
|
1246
1340
|
}
|
|
1247
1341
|
return {structureMatch, entry: {layout, structure: structureMatch ? 'match' : 'changed', record: clone(recordValue), ...(catalogRecord !== undefined ? {catalogRecord: clone(catalogRecord)} : {})}};
|
|
1248
1342
|
};
|
|
1343
|
+
// A record a slide carried is embedded under its group (declared with its source when the document does not declare it)
|
|
1344
|
+
// while a restored slide still references it and the document does not already resolve it.
|
|
1249
1345
|
const finalize = doc => {
|
|
1250
|
-
const
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1346
|
+
for (const [layout, item] of resolved) {
|
|
1347
|
+
if (!item.add || !array(doc.slides).some(entry => entry?.layout === layout) || resolveReference(doc, 'layouts', layout, {catalogs: []})) continue;
|
|
1348
|
+
const {group: name, id, source} = item.add;
|
|
1349
|
+
const catalogs = object(doc.catalogs) ? {...doc.catalogs} : {};
|
|
1350
|
+
const current = catalogs[name];
|
|
1351
|
+
if (current === false) continue;
|
|
1352
|
+
const holder = object(current) ? {...current} : (name !== 'custom' && source !== undefined ? {source} : {});
|
|
1353
|
+
holder.layouts = {...(object(holder.layouts) ? holder.layouts : {}), [id]: clone(item.record)};
|
|
1354
|
+
catalogs[name] = holder;
|
|
1355
|
+
doc.catalogs = catalogs;
|
|
1356
|
+
}
|
|
1254
1357
|
return doc;
|
|
1255
1358
|
};
|
|
1256
1359
|
return {slide, finalize};
|