@openpresentation/opf-pptx 0.11.5 → 0.11.7
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 +22 -8
- package/dist/background.js +15 -13
- package/dist/chart-types.js +5 -1
- package/dist/chartex.js +40 -24
- package/dist/code-provenance.js +1 -1
- package/dist/content-topology.js +295 -0
- package/dist/document-provenance.js +260 -46
- package/dist/furniture-provenance.js +57 -4
- package/dist/index.d.ts +21 -12
- package/dist/index.js +380 -65
- package/dist/logo-provenance.js +173 -0
- package/dist/sections.js +111 -0
- package/dist/watermark-provenance.js +2 -1
- package/package.json +7 -6
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import {XMLParser} from 'fast-xml-parser';
|
|
2
|
+
import {decodeTextTag, encodeTextTag} from './code-provenance.js';
|
|
3
|
+
import {pictureTransform} from './image-geometry.js';
|
|
4
|
+
|
|
5
|
+
// The deck logo (core's `geometry.logo`: design.logo, a slide's own logo or the
|
|
6
|
+
// primary organization's logo on a cover or section slide) exports as one native
|
|
7
|
+
// picture per slide named `OPF logo`, after the watermark and before content. It
|
|
8
|
+
// is fitted into core's box without cropping, anchored left and vertically
|
|
9
|
+
// centered (the preview's `xMinYMid meet`). The native tag records the exact
|
|
10
|
+
// picture, so an unchanged picture is consumed on import (the logo itself returns
|
|
11
|
+
// from the document and slide design records) while an edited one stays an
|
|
12
|
+
// ordinary picture and reports `invalid-logo-provenance`.
|
|
13
|
+
const TAG = 'OPF_LOGO_V1', REL = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/tags';
|
|
14
|
+
const EMUS_PER_INCH = 914400;
|
|
15
|
+
const parser = new XMLParser({ignoreAttributes:false, attributeNamePrefix:'', parseTagValue:false, trimValues:false});
|
|
16
|
+
const decoder = new TextDecoder('utf-8', {fatal:true}), encoder = new TextEncoder();
|
|
17
|
+
const array = value => value === undefined ? [] : Array.isArray(value) ? value : [value];
|
|
18
|
+
// Whitespace-only text between elements is not identity: the vendored writer indents, PowerPoint drops it on save.
|
|
19
|
+
const canonical = value => JSON.stringify(value, (_key, item) => item && typeof item === 'object' && !Array.isArray(item)
|
|
20
|
+
? Object.fromEntries(Object.keys(item).sort().filter(key => !(key === '#text' && typeof item[key] === 'string' && !item[key].trim())).map(key => [key, item[key]])) : item);
|
|
21
|
+
// The OPF paths core reports for a logo source.
|
|
22
|
+
const LOGO_PATH = /^(?:slides\.\d{1,9}\.design\.logo|design\.logo|organization(?:\.\d{1,9})?\.logo)(?:\.[A-Za-z]{1,16})?$/;
|
|
23
|
+
|
|
24
|
+
export const LOGO_TAG = TAG;
|
|
25
|
+
export const logoName = () => 'OPF logo';
|
|
26
|
+
|
|
27
|
+
function blipFillIdentity(blipFill) {
|
|
28
|
+
const {['a:blip']: blip, ...rest} = blipFill ?? {};
|
|
29
|
+
const {['r:embed']: _embed, ...blipRest} = blip ?? {};
|
|
30
|
+
return {...rest, 'a:blip': blipRest};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The picture frame inside core's logo box: contained, anchored at the box's
|
|
35
|
+
* left edge and vertically centered. A 90 degree EXIF orientation swaps the
|
|
36
|
+
* frame's axes, so its visual width is the stored height.
|
|
37
|
+
*/
|
|
38
|
+
export function logoFrame(metadata, box) {
|
|
39
|
+
const placed = pictureTransform(metadata, box, 'fit');
|
|
40
|
+
const visualWidth = (metadata.orientation ?? 1) >= 5 ? placed.h : placed.w;
|
|
41
|
+
return {...placed, x: placed.x - (box.w - visualWidth) / 2};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Place each generated logo picture at its frame and bind it to its manifest
|
|
46
|
+
* with a native tag. logos: Map<slidePart, {slide, box (inches), path, variant}>.
|
|
47
|
+
*/
|
|
48
|
+
export function placeLogos(entries, logos, metadataFor, fail) {
|
|
49
|
+
if (!logos.size) return;
|
|
50
|
+
const manifests = new Map();
|
|
51
|
+
for (const part of Object.keys(entries).filter(path => /^ppt\/slides\/slide\d+\.xml$/.test(path))) {
|
|
52
|
+
const logo = logos.get(part);
|
|
53
|
+
if (!logo) continue;
|
|
54
|
+
const xml = decoder.decode(entries[part]);
|
|
55
|
+
const emu = value => Math.round(value * EMUS_PER_INCH);
|
|
56
|
+
let found = 0;
|
|
57
|
+
const next = xml.replace(/<p:pic>[\s\S]*?<\/p:pic>/g, picture => {
|
|
58
|
+
if (picture.match(/<p:cNvPr\b[^>]*\bname="([^"]+)"/)?.[1] !== logoName()) return picture;
|
|
59
|
+
found++;
|
|
60
|
+
const embed = picture.match(/<a:blip\b[^>]*r:embed="([^"]+)"/)?.[1];
|
|
61
|
+
const metadata = metadataFor(part, embed);
|
|
62
|
+
if (!metadata) fail(logo.path);
|
|
63
|
+
const placed = logoFrame(metadata, logo.box);
|
|
64
|
+
const attributes = `${placed.rotation ? ` rot="${placed.rotation * 60000}"` : ''}${placed.flipH ? ' flipH="1"' : ''}${placed.flipV ? ' flipV="1"' : ''}`;
|
|
65
|
+
picture = picture.replace(/<a:xfrm\b[^>]*>[\s\S]*?<\/a:xfrm>/, `<a:xfrm${attributes}><a:off x="${emu(placed.x)}" y="${emu(placed.y)}"/><a:ext cx="${emu(placed.w)}" cy="${emu(placed.h)}"/></a:xfrm>`);
|
|
66
|
+
picture = picture.replace(/<a:srcRect\b[^>]*\/>/, '');
|
|
67
|
+
const node = parser.parse(picture)['p:pic'];
|
|
68
|
+
manifests.set(part, {v: 1, slide: logo.slide, path: logo.path, variant: logo.variant, properties: node['p:spPr'], blipFill: blipFillIdentity(node['p:blipFill'])});
|
|
69
|
+
return picture;
|
|
70
|
+
});
|
|
71
|
+
if (found !== 1) throw new Error('Missing generated logo picture.');
|
|
72
|
+
entries[part] = encoder.encode(next);
|
|
73
|
+
}
|
|
74
|
+
attachTags(entries, manifests);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// One tag part per slide, related from the picture's application properties.
|
|
78
|
+
function attachTags(entries, manifests) {
|
|
79
|
+
const NS = 'http://schemas.openxmlformats.org/presentationml/2006/main', types = [];
|
|
80
|
+
let count = 0;
|
|
81
|
+
for (const [part, manifest] of manifests) {
|
|
82
|
+
const relPath = part.replace('/slides/', '/slides/_rels/') + '.rels';
|
|
83
|
+
const rels = decoder.decode(entries[relPath]);
|
|
84
|
+
const ids = new Set([...rels.matchAll(/\bId="([^"]+)"/g)].map(match => match[1]));
|
|
85
|
+
const tagPart = `ppt/tags/opfLogo${++count}.xml`;
|
|
86
|
+
let id = `rIdOpfLogo${count}`;
|
|
87
|
+
while (ids.has(id)) id += '_';
|
|
88
|
+
entries[tagPart] = encoder.encode(`<?xml version="1.0" encoding="UTF-8" standalone="yes"?><p:tagLst xmlns:p="${NS}"><p:tag name="${TAG}" val="${encodeTextTag(manifest)}"/></p:tagLst>`);
|
|
89
|
+
types.push(`<Override PartName="/${tagPart}" ContentType="application/vnd.openxmlformats-officedocument.presentationml.tags+xml"/>`);
|
|
90
|
+
entries[relPath] = encoder.encode(rels.replace('</Relationships>', `<Relationship Id="${id}" Type="${REL}" Target="../tags/opfLogo${count}.xml"/></Relationships>`));
|
|
91
|
+
let attached = false;
|
|
92
|
+
const xml = decoder.decode(entries[part]).replace(/<p:pic>[\s\S]*?<\/p:pic>/g, picture => {
|
|
93
|
+
if (attached || picture.match(/<p:cNvPr\b[^>]*\bname="([^"]+)"/)?.[1] !== logoName()) return picture;
|
|
94
|
+
attached = true;
|
|
95
|
+
picture = picture.replace(/<p:nvPr\s*\/>/, '<p:nvPr></p:nvPr>');
|
|
96
|
+
if (!picture.includes('</p:nvPr>')) throw new Error('Generated logo picture has no native application properties.');
|
|
97
|
+
return picture.replace('</p:nvPr>', `<p:custDataLst><p:tags r:id="${id}"/></p:custDataLst></p:nvPr>`);
|
|
98
|
+
});
|
|
99
|
+
entries[part] = encoder.encode(xml);
|
|
100
|
+
}
|
|
101
|
+
entries['[Content_Types].xml'] = encoder.encode(decoder.decode(entries['[Content_Types].xml']).replace('</Types>', types.join('') + '</Types>'));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function readTags(container, relationships, entries) {
|
|
105
|
+
const tags = [];
|
|
106
|
+
let unreadable = false;
|
|
107
|
+
for (const link of array(container?.['p:tags'])) {
|
|
108
|
+
const rel = relationships.get(link['r:id']);
|
|
109
|
+
if (rel?.type !== REL || rel.targetMode === 'External' || !entries[rel.path]) { unreadable = true; continue; }
|
|
110
|
+
try { tags.push(...array(parser.parse(decoder.decode(entries[rel.path]))['p:tagLst']?.['p:tag'])); } catch { unreadable = true; }
|
|
111
|
+
}
|
|
112
|
+
return {tags, unreadable};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* The panel an unresolved logo draws is a rectangle named "OPF image placeholder N" carrying a role "placeholder" logo
|
|
117
|
+
* tag for this slide, plus its text lines and icon strokes named after it. A valid tag consumes all of them: they are
|
|
118
|
+
* a status indicator, not content. Without a valid tag they stay ordinary shapes. Returns the consumed shape indexes.
|
|
119
|
+
*/
|
|
120
|
+
export function importLogoPlaceholders(shapes, relationships, entries, slideIndex) {
|
|
121
|
+
const consumed = new Set(), names = shapes.map(shape => shape['p:nvSpPr']?.['p:cNvPr']?.name);
|
|
122
|
+
for (const [index, shape] of shapes.entries()) {
|
|
123
|
+
const name = names[index];
|
|
124
|
+
if (typeof name !== 'string' || !/^OPF image placeholder \d+$/.test(name)) continue;
|
|
125
|
+
const {tags} = readTags(shape['p:nvSpPr']?.['p:nvPr']?.['p:custDataLst'], relationships, entries);
|
|
126
|
+
const own = tags.filter(tag => tag.name?.toUpperCase() === TAG);
|
|
127
|
+
try {
|
|
128
|
+
if (own.length !== 1) continue;
|
|
129
|
+
const manifest = decodeTextTag(own[0].val);
|
|
130
|
+
if (manifest?.v !== 1 || manifest.role !== 'placeholder' || manifest.slide !== `slides.${slideIndex}` || typeof manifest.path !== 'string' || !LOGO_PATH.test(manifest.path)) continue;
|
|
131
|
+
} catch { continue; }
|
|
132
|
+
consumed.add(index);
|
|
133
|
+
for (const [other, otherName] of names.entries()) if (typeof otherName === 'string' && otherName.startsWith(`${name} text line `) || otherName?.startsWith(`${name} icon `)) consumed.add(other);
|
|
134
|
+
}
|
|
135
|
+
return consumed;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Consume an unchanged tagged logo picture: it is generated from design.logo (or
|
|
140
|
+
* the organization's logo), never content. Returns the consumed picture indexes
|
|
141
|
+
* and `fallback`, the picture's own `{path, image}`: the caller uses it for the
|
|
142
|
+
* logo only when the stored document and slide design did not restore one (an
|
|
143
|
+
* export without provenance, or one that omitted the field), so the logo is never
|
|
144
|
+
* lost. An edited, ambiguous or damaged tagged picture stays an ordinary picture
|
|
145
|
+
* and is reported. readPicture(picture) returns the ordinary imported payload.
|
|
146
|
+
*/
|
|
147
|
+
export function importLogo(pictures, relationships, entries, slideIndex, readPicture, report) {
|
|
148
|
+
const consumed = new Set(), found = [];
|
|
149
|
+
for (const [index, picture] of pictures.entries()) {
|
|
150
|
+
const {tags, unreadable} = readTags(picture['p:nvPicPr']?.['p:nvPr']?.['p:custDataLst'], relationships, entries);
|
|
151
|
+
const own = tags.filter(tag => tag.name?.toUpperCase() === TAG);
|
|
152
|
+
// A header/footer logo picture carries the same tag with role "furniture"; furniture import owns it.
|
|
153
|
+
if (!own.length || own.every(tag => { try { return decodeTextTag(tag.val)?.role === 'furniture'; } catch { return false; } })) continue;
|
|
154
|
+
found.push({index, picture, own, unreadable, others: tags.some(tag => /^OPF_/i.test(tag.name) && tag.name.toUpperCase() !== TAG)});
|
|
155
|
+
}
|
|
156
|
+
if (!found.length) return {consumed};
|
|
157
|
+
const invalid = () => report({code: 'invalid-logo-provenance', message: 'An edited, ambiguous or invalid tagged OPF logo was imported as an ordinary picture. design.logo returns only from the stored document design.'});
|
|
158
|
+
if (found.length > 1) { invalid(); return {consumed}; }
|
|
159
|
+
const [{index, picture, own, unreadable, others}] = found;
|
|
160
|
+
try {
|
|
161
|
+
if (unreadable || others || own.length !== 1) throw Error('Ambiguous logo identity.');
|
|
162
|
+
const manifest = decodeTextTag(own[0].val);
|
|
163
|
+
if (manifest?.v !== 1 || manifest.slide !== `slides.${slideIndex}` || typeof manifest.path !== 'string' || !LOGO_PATH.test(manifest.path) || typeof manifest.variant !== 'string' || manifest.variant.length > 32) throw Error('Invalid logo manifest.');
|
|
164
|
+
if (picture['p:nvPicPr']?.['p:cNvPr']?.name !== logoName()) throw Error('Logo identity changed.');
|
|
165
|
+
if (canonical(picture['p:spPr']) !== canonical(manifest.properties) || canonical(blipFillIdentity(picture['p:blipFill'])) !== canonical(manifest.blipFill)) throw Error('Logo geometry changed.');
|
|
166
|
+
const image = readPicture(picture)?.payload?.image;
|
|
167
|
+
consumed.add(index);
|
|
168
|
+
return {consumed, ...(typeof image?.src === 'string' ? {fallback: {path: manifest.path, image: {src: image.src, ...(typeof image.alt === 'string' ? {alt: image.alt} : {})}}} : {})};
|
|
169
|
+
} catch {
|
|
170
|
+
invalid();
|
|
171
|
+
return {consumed};
|
|
172
|
+
}
|
|
173
|
+
}
|
package/dist/sections.js
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// Native PowerPoint sections (spec-gap closure P1).
|
|
2
|
+
//
|
|
3
|
+
// A slide's `section` is PowerPoint's own section label. It is written as the
|
|
4
|
+
// PowerPoint 2010 section list, `p14:sectionLst` inside `p:presentation/
|
|
5
|
+
// p:extLst`, the structure PowerPoint shows in its sections pane and keeps
|
|
6
|
+
// through saves. Each maximal run of consecutive slides with the same `section`
|
|
7
|
+
// is one section; runs of slides without a section become `Default Section`,
|
|
8
|
+
// PowerPoint's own name for the section it creates when a deck first gets one,
|
|
9
|
+
// so every slide is covered (the list is only valid when it is complete).
|
|
10
|
+
// Section ids are deterministic GUIDs derived from the run index and name.
|
|
11
|
+
//
|
|
12
|
+
// Import reads the list back: a slide in a section named exactly `Default
|
|
13
|
+
// Section` has no `section`; any other name is the slide's `section`. The
|
|
14
|
+
// native list wins over the stored OPF_SLIDE_V1 value and over the section
|
|
15
|
+
// text a footer shows, since PowerPoint edits the list, not the hidden tags.
|
|
16
|
+
|
|
17
|
+
import {hashText} from './document-provenance.js';
|
|
18
|
+
|
|
19
|
+
export const DEFAULT_SECTION = 'Default Section';
|
|
20
|
+
const SECTION_EXT_URI = '{521415D9-36F7-43E2-AB2F-B90AF26B5E84}';
|
|
21
|
+
const P14 = 'http://schemas.microsoft.com/office/powerpoint/2010/main';
|
|
22
|
+
|
|
23
|
+
const array = value => value === undefined ? [] : Array.isArray(value) ? value : [value];
|
|
24
|
+
const object = value => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
25
|
+
const escape = text => String(text).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
26
|
+
// Characters XML 1.0 cannot carry; the same guard the text runs use (invalid-text).
|
|
27
|
+
export const INVALID_XML_CHARACTER = /[\u0000-\u0008\u000B\u000C\u000E-\u001F\uD800-\uDFFF]/u;
|
|
28
|
+
// An attribute value is normalized by every XML reader (tab, LF and CR become
|
|
29
|
+
// spaces), so a re-saved list would differ from the authored label. Write the
|
|
30
|
+
// normalized form, and compare labels the same way (`sameSectionName`).
|
|
31
|
+
export const normalizeSectionName = value => String(value).replace(/[\t\n\r]/g, ' ');
|
|
32
|
+
export const sameSectionName = (a, b) => normalizeSectionName(a) === normalizeSectionName(b);
|
|
33
|
+
|
|
34
|
+
// A blank label names no section.
|
|
35
|
+
const named = value => typeof value === 'string' && value.trim() !== '';
|
|
36
|
+
|
|
37
|
+
/** Maximal runs of consecutive slides with the same section: [{name, slides: [index]}]. */
|
|
38
|
+
export function sectionRuns(sections) {
|
|
39
|
+
const runs = [];
|
|
40
|
+
for (const [index, value] of sections.entries()) {
|
|
41
|
+
const name = named(value) ? value : DEFAULT_SECTION;
|
|
42
|
+
const last = runs.at(-1);
|
|
43
|
+
if (last && last.name === name) last.slides.push(index);
|
|
44
|
+
else runs.push({name, slides: [index]});
|
|
45
|
+
}
|
|
46
|
+
return runs;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Change detection hashes (cyrb53) formatted as a version-4-shaped GUID: stable
|
|
50
|
+
// for the same run index and name, which keeps the package deterministic.
|
|
51
|
+
export function sectionId(index, name) {
|
|
52
|
+
const first = hashText(`${index}\u0000${name}`), second = hashText(`${name}\u0000${index}\u0000section`);
|
|
53
|
+
const hex = (first + second + hashText(first + second)).toUpperCase();
|
|
54
|
+
return `{${hex.slice(0, 8)}-${hex.slice(8, 12)}-4${hex.slice(12, 15)}-8${hex.slice(15, 18)}-${hex.slice(18, 30)}}`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Insert the section list into `presentationXml` for the per-slide `sections`
|
|
59
|
+
* (string or undefined, in slide order). Returns the XML unchanged when no
|
|
60
|
+
* slide has a section. The `p:sldId` ids are read from the presentation part,
|
|
61
|
+
* which must list exactly one id per slide.
|
|
62
|
+
*/
|
|
63
|
+
export function writeSectionList(presentationXml, sections) {
|
|
64
|
+
if (!sections.some(named)) return presentationXml;
|
|
65
|
+
const ids = [...presentationXml.matchAll(/<p:sldId\b[^>]*\bid="(\d+)"/g)].map(match => match[1]);
|
|
66
|
+
if (ids.length !== sections.length) throw Error('Generated slide id count differs from the document.');
|
|
67
|
+
if (/<p14:sectionLst\b/.test(presentationXml)) throw Error('Generated presentation already has a section list.');
|
|
68
|
+
for (const [index, name] of sections.entries()) {
|
|
69
|
+
const invalid = named(name) ? INVALID_XML_CHARACTER.exec(name) : null;
|
|
70
|
+
if (invalid) throw Error(`slides.${index}.section contains U+${invalid[0].codePointAt(0).toString(16).toUpperCase().padStart(4, '0')} at UTF-16 offset ${invalid.index}, which PresentationML XML cannot represent.`);
|
|
71
|
+
}
|
|
72
|
+
const list = sectionRuns(sections.map(name => named(name) ? normalizeSectionName(name) : name)).map((run, index) =>
|
|
73
|
+
`<p14:section name="${escape(run.name)}" id="${sectionId(index, run.name)}"><p14:sldIdLst>${run.slides.map(slide => `<p14:sldId id="${ids[slide]}"/>`).join('')}</p14:sldIdLst></p14:section>`).join('');
|
|
74
|
+
const ext = `<p:ext uri="${SECTION_EXT_URI}"><p14:sectionLst xmlns:p14="${P14}">${list}</p14:sectionLst></p:ext>`;
|
|
75
|
+
// CT_Presentation: extLst is the last child. Join an existing list, else add one.
|
|
76
|
+
if (presentationXml.includes('</p:extLst>')) return presentationXml.replace('</p:extLst>', `${ext}</p:extLst>`);
|
|
77
|
+
const close = presentationXml.lastIndexOf('</p:presentation>');
|
|
78
|
+
if (close < 0) throw Error('Generated presentation part has no closing element.');
|
|
79
|
+
return `${presentationXml.slice(0, close)}<p:extLst>${ext}</p:extLst>${presentationXml.slice(close)}`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* The native section list of a parsed `p:presentation` root: a Map from
|
|
84
|
+
* `p:sldId` id to section name, or null when the package has no section list.
|
|
85
|
+
* Names are untrusted strings; a malformed list counts as none.
|
|
86
|
+
*/
|
|
87
|
+
export function readSectionList(presentationRoot) {
|
|
88
|
+
const ext = array(presentationRoot?.['p:extLst']?.['p:ext']).find(item => object(item) && item['p14:sectionLst'] !== undefined);
|
|
89
|
+
if (!ext) return null;
|
|
90
|
+
const list = ext['p14:sectionLst'];
|
|
91
|
+
if (!object(list)) return null;
|
|
92
|
+
const map = new Map();
|
|
93
|
+
for (const section of array(list['p14:section'])) {
|
|
94
|
+
if (!object(section) || typeof section.name !== 'string') continue;
|
|
95
|
+
for (const slide of array(section['p14:sldIdLst']?.['p14:sldId'])) {
|
|
96
|
+
const id = object(slide) ? slide.id : undefined;
|
|
97
|
+
if (typeof id === 'string' && /^\d+$/.test(id) && !map.has(id)) map.set(id, section.name);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return map;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Per-slide section names from the native list, aligned with `slideIds` (the `p:sldId` ids in slide order); null without a list. */
|
|
104
|
+
export function nativeSections(presentationRoot, slideIds) {
|
|
105
|
+
const map = readSectionList(presentationRoot);
|
|
106
|
+
if (!map) return null;
|
|
107
|
+
return slideIds.map(id => {
|
|
108
|
+
const name = map.get(String(id));
|
|
109
|
+
return name === undefined || name === DEFAULT_SECTION ? undefined : name;
|
|
110
|
+
});
|
|
111
|
+
}
|
|
@@ -13,8 +13,9 @@ export const WATERMARK_DEFAULT_OPACITY = 0.08;
|
|
|
13
13
|
const parser = new XMLParser({ignoreAttributes:false, attributeNamePrefix:'', parseTagValue:false, trimValues:false});
|
|
14
14
|
const decoder = new TextDecoder('utf-8', {fatal:true}), encoder = new TextEncoder();
|
|
15
15
|
const array = value => value === undefined ? [] : Array.isArray(value) ? value : [value];
|
|
16
|
+
// Whitespace-only text between elements is not identity: the vendored writer indents, PowerPoint drops it on save.
|
|
16
17
|
const canonical = value => JSON.stringify(value, (_key, item) => item && typeof item === 'object' && !Array.isArray(item)
|
|
17
|
-
? Object.fromEntries(Object.keys(item).sort().map(key => [key, item[key]])) : item);
|
|
18
|
+
? Object.fromEntries(Object.keys(item).sort().filter(key => !(key === '#text' && typeof item[key] === 'string' && !item[key].trim())).map(key => [key, item[key]])) : item);
|
|
18
19
|
|
|
19
20
|
export const watermarkName = () => 'OPF watermark';
|
|
20
21
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openpresentation/opf-pptx",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.7",
|
|
4
4
|
"description": "Pure local OPF to PPTX export and PPTX to OPF import tooling.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -37,11 +37,12 @@
|
|
|
37
37
|
"sideEffects": false,
|
|
38
38
|
"scripts": {
|
|
39
39
|
"build": "node scripts/build.mjs",
|
|
40
|
-
"typecheck": "node --check src/index.js && node --check src/chart-types.js && node --check src/chartex.js && node --check test/chartex.mjs && node --check test/chart-types.mjs && node --check test/design-alignment.mjs && node --check src/color-ref.js && node --check src/theme-colors.js && node --check test/theme-colors.mjs && node --check test/tag-colour.mjs && node --check test/table-theme-colors.mjs && node --check scripts/build.mjs && node --check scripts/validate-package.mjs && node --check test/smoke.mjs && node --check src/image-geometry.js && node --check src/slide-image-provenance.js && node --check test/slide-image.mjs && node --check test/slide-image-treatments.mjs && node --check src/watermark-provenance.js && node --check src/media-dedupe.js && node --check test/media-dedupe.mjs && node --check test/watermark.mjs && node --check test/image-fit.mjs && node --check test/image-orientation.mjs && node --check test/table-layout.mjs && node --check test/table-import.mjs && node --check test/object-ids.mjs && node --check test/export-corpus.mjs && node --check test/image-media-type.mjs && node --check src/image-fallback-node.js && node --check src/image-fallback-browser.js && node --check test/webp-fallback.mjs && node --check scripts/build-browser-check.mjs && node --check test/webp-fallback-browser.js && node --check test/image-fallback-boundary.mjs && node --check src/background.js && node --check test/background.mjs && node --check src/image-import.js && node --check test/image-import.mjs && node --check test/native-background.mjs && node --check src/background-import.js && node --check test/background-inheritance.mjs && node --check test/rich-table.mjs && node --check src/table-import.js && node --check test/rich-table-import.mjs && node --check test/rich-table-import-browser.js && node --check test/table-row-sizing.mjs && node --check test/table-styles.mjs && node --check test/table-styles-browser.js && node --check test/styled-table.mjs && node --check src/table-cell-import.js && node --check test/styled-table-import.mjs && node --check test/styled-table-import-browser.js && node --check src/table-border-import.js && node --check test/table-border-styles.mjs && node --check src/card-provenance.js && node --check test/content-cards.mjs && node --check src/chart-workbook.js && node --check test/chart-colors.mjs && node --check test/chart-workbook.mjs && node --check test/single-column-chart.mjs && node --check test/native-chart-colors.mjs && node --check src/heading-provenance.js && node --check test/accepted-text.mjs && node --check test/heading-provenance.mjs && node --check src/text-provenance.js && node --check test/plain-whitespace.mjs && node --check src/timeline-provenance.js && node --check src/quote-provenance.js && node --check test/quote-provenance.mjs && node --check test/shared-timeline.mjs && node --check test/shared-furniture.mjs && node --check src/furniture-provenance.js && node --check src/document-provenance.js && node --check test/document-provenance.mjs && node --check test/furniture-provenance.mjs && node --check src/furniture-fields.js && node --check test/furniture-fields.mjs && node --check test/furniture-wrapped-date.mjs && node --check test/native-picture.mjs && node --check test/compare-native-picture.mjs && node --check test/compare-native-picture-edit.mjs && node --check test/compare-native-picture-edit-controls.mjs && node --check test/native-font-embed-audit.mjs && node --check test/native-font-embed-controls.mjs && node --check test/native-font-embed-fixture-source.mjs && node --check test/native-font-edit-fixture.mjs && node --check test/native-mixed-edit-audit.mjs && node --check test/native-mixed-edit-controls.mjs && node --check test/master-bullet-font.mjs && node --check test/native-font-inventory-audit.mjs && node --check test/native-font-inventory-controls.mjs && node --check test/default-font-scheme.mjs && node --check test/export-chosen-fonts.mjs && node --check test/background-fills.mjs && node --check src/typeface-inventory.js && node --check src/package-fonts.js && node --check test/typeface-inventory.mjs && node --check src/script-fonts.js && node --check test/script-fonts.mjs && node --check test/theme-script-slots.mjs && node --check test/powershell-scan.mjs && node --check test/powershell-scan-probes.mjs && node --check test/socials-furniture.mjs && node --check src/media-provenance.js && node --check test/layout-parity.mjs && node --check test/cover-centering.mjs && node --check test/media-placeholder.mjs && node --check test/image-placeholder.mjs && node --check test/list-shape-names.mjs && node --check test/layout-intent.mjs && node --check test/chart-cache-import.mjs && node --check test/zip-date.mjs && node --check test/native-text-whitespace.mjs && node --check test/native-underline.mjs && node --check src/native-text-style.js && node --check src/font-weights.js && node --check test/font-weight-import.mjs && node --check src/body-text-import.js && node --check test/native-body-rich-text.mjs && node --check test/native-body-rich-text-browser.js && node --check test/native-quote-import-contract.mjs && node --check test/native-quote-import-controls.mjs && node --check test/native-quote.mjs && node --check test/determinism.mjs && node --check test/determinism-fixtures.mjs && node --check test/table-frame-furniture-order.mjs && node --check test/shape-border-widths.mjs && node --check test/gallery-layout-alignment.mjs",
|
|
41
|
-
"test": "npm run build && node test/chart-types.mjs && node test/chartex.mjs && node test/zip-date.mjs && node test/background-fills.mjs && npm run test:furniture && node test/slide-image.mjs && node test/slide-image-treatments.mjs && node test/watermark.mjs && node test/media-dedupe.mjs && node test/document-provenance.mjs && node test/layout-intent.mjs && node test/master-bullet-font.mjs && node test/default-font-scheme.mjs && node test/script-fonts.mjs && node test/theme-script-slots.mjs && node test/export-chosen-fonts.mjs && node test/typeface-inventory.mjs && node test/color-ref-export.mjs && node test/theme-colors.mjs && node test/tag-colour.mjs && node test/table-theme-colors.mjs && node test/plain-whitespace.mjs && node test/native-text-whitespace.mjs && node test/native-body-rich-text.mjs && node test/native-underline.mjs && node test/font-weight-import.mjs && node test/design-alignment.mjs && node test/accepted-text.mjs && node test/heading-provenance.mjs && node test/native-quote-import-controls.mjs && node test/chart-colors.mjs && node test/content-cards.mjs && node test/chart-workbook.mjs && node test/single-column-chart.mjs && node --test test/chart-cache-import.mjs && node test/dependency-boundary.mjs && npm run build:browser-check && npm run test:styled-table && node test/content-layout.mjs && node test/layout-parity.mjs && node test/gallery-layout-alignment.mjs && node test/cover-centering.mjs && node test/table-frame-furniture-order.mjs && node test/media-placeholder.mjs && node test/shape-border-widths.mjs && node test/image-placeholder.mjs && node test/list-shape-names.mjs && node test/shared-quote.mjs && node test/quote-provenance.mjs && npm run test:timeline && npm run test:determinism",
|
|
40
|
+
"typecheck": "node --check src/index.js && node --check src/chart-types.js && node --check src/chartex.js && node --check test/chartex.mjs && node --check test/chart-text-size.mjs && node --check test/chart-types.mjs && node --check test/design-alignment.mjs && node --check src/color-ref.js && node --check src/theme-colors.js && node --check test/theme-colors.mjs && node --check test/tag-colour.mjs && node --check test/table-theme-colors.mjs && node --check scripts/build.mjs && node --check scripts/validate-package.mjs && node --check test/smoke.mjs && node --check src/image-geometry.js && node --check src/slide-image-provenance.js && node --check test/slide-image.mjs && node --check test/slide-image-treatments.mjs && node --check src/watermark-provenance.js && node --check src/logo-provenance.js && node --check test/design-fields.mjs && node --check src/media-dedupe.js && node --check test/media-dedupe.mjs && node --check test/watermark.mjs && node --check test/image-fit.mjs && node --check test/image-orientation.mjs && node --check test/table-layout.mjs && node --check test/table-import.mjs && node --check test/object-ids.mjs && node --check test/export-corpus.mjs && node --check test/image-media-type.mjs && node --check src/image-fallback-node.js && node --check src/image-fallback-browser.js && node --check test/webp-fallback.mjs && node --check scripts/build-browser-check.mjs && node --check test/webp-fallback-browser.js && node --check test/image-fallback-boundary.mjs && node --check src/background.js && node --check test/background.mjs && node --check src/image-import.js && node --check test/image-import.mjs && node --check test/native-background.mjs && node --check src/background-import.js && node --check test/background-inheritance.mjs && node --check test/rich-table.mjs && node --check src/table-import.js && node --check test/rich-table-import.mjs && node --check test/rich-table-import-browser.js && node --check test/table-row-sizing.mjs && node --check test/table-styles.mjs && node --check test/table-styles-browser.js && node --check test/styled-table.mjs && node --check src/table-cell-import.js && node --check test/styled-table-import.mjs && node --check test/styled-table-import-browser.js && node --check src/table-border-import.js && node --check test/table-border-styles.mjs && node --check src/card-provenance.js && node --check test/content-cards.mjs && node --check src/chart-workbook.js && node --check test/chart-colors.mjs && node --check test/chart-workbook.mjs && node --check test/single-column-chart.mjs && node --check test/native-chart-colors.mjs && node --check src/heading-provenance.js && node --check test/accepted-text.mjs && node --check test/heading-provenance.mjs && node --check src/text-provenance.js && node --check test/plain-whitespace.mjs && node --check src/timeline-provenance.js && node --check src/quote-provenance.js && node --check test/quote-provenance.mjs && node --check test/shared-timeline.mjs && node --check test/shared-furniture.mjs && node --check src/furniture-provenance.js && node --check src/document-provenance.js && node --check src/content-topology.js && node --check src/sections.js && node --check test/document-provenance.mjs && node --check test/content-topology.mjs && node --check test/sections.mjs && node --check test/provenance-interop.mjs && node --check test/furniture-provenance.mjs && node --check src/furniture-fields.js && node --check test/furniture-fields.mjs && node --check test/furniture-wrapped-date.mjs && node --check test/native-picture.mjs && node --check test/compare-native-picture.mjs && node --check test/compare-native-picture-edit.mjs && node --check test/compare-native-picture-edit-controls.mjs && node --check test/native-font-embed-audit.mjs && node --check test/native-font-embed-controls.mjs && node --check test/native-font-embed-fixture-source.mjs && node --check test/native-font-edit-fixture.mjs && node --check test/native-mixed-edit-audit.mjs && node --check test/native-mixed-edit-controls.mjs && node --check test/master-bullet-font.mjs && node --check test/native-font-inventory-audit.mjs && node --check test/native-font-inventory-controls.mjs && node --check test/default-font-scheme.mjs && node --check test/export-chosen-fonts.mjs && node --check test/background-fills.mjs && node --check src/typeface-inventory.js && node --check src/package-fonts.js && node --check test/typeface-inventory.mjs && node --check src/script-fonts.js && node --check test/script-fonts.mjs && node --check test/theme-script-slots.mjs && node --check test/powershell-scan.mjs && node --check test/powershell-scan-probes.mjs && node --check test/socials-furniture.mjs && node --check src/media-provenance.js && node --check test/layout-parity.mjs && node --check test/cover-centering.mjs && node --check test/media-placeholder.mjs && node --check test/image-placeholder.mjs && node --check test/svg-image-degrade.mjs && node --check test/background-colorref.mjs && node --check test/import-fabrications.mjs && node --check test/list-shape-names.mjs && node --check test/layout-intent.mjs && node --check test/chart-cache-import.mjs && node --check test/zip-date.mjs && node --check test/native-text-whitespace.mjs && node --check test/native-underline.mjs && node --check src/native-text-style.js && node --check src/font-weights.js && node --check test/font-weight-import.mjs && node --check src/body-text-import.js && node --check test/native-body-rich-text.mjs && node --check test/native-body-rich-text-browser.js && node --check test/native-quote-import-contract.mjs && node --check test/native-quote-import-controls.mjs && node --check test/native-quote.mjs && node --check test/determinism.mjs && node --check test/determinism-fixtures.mjs && node --check test/table-frame-furniture-order.mjs && node --check test/shape-border-widths.mjs && node --check test/gallery-layout-alignment.mjs",
|
|
41
|
+
"test": "npm run build && node test/chart-types.mjs && node test/chartex.mjs && node test/chart-text-size.mjs && node test/zip-date.mjs && node test/background-fills.mjs && npm run test:furniture && node test/slide-image.mjs && node test/slide-image-treatments.mjs && node test/watermark.mjs && node test/design-fields.mjs && node test/media-dedupe.mjs && node test/document-provenance.mjs && node test/content-topology.mjs && node test/sections.mjs && node test/layout-intent.mjs && node test/master-bullet-font.mjs && node test/default-font-scheme.mjs && node test/script-fonts.mjs && node test/theme-script-slots.mjs && node test/export-chosen-fonts.mjs && node test/typeface-inventory.mjs && node test/color-ref-export.mjs && node test/theme-colors.mjs && node test/tag-colour.mjs && node test/table-theme-colors.mjs && node test/plain-whitespace.mjs && node test/native-text-whitespace.mjs && node test/native-body-rich-text.mjs && node test/native-underline.mjs && node test/font-weight-import.mjs && node test/design-alignment.mjs && node test/accepted-text.mjs && node test/heading-provenance.mjs && node test/native-quote-import-controls.mjs && node test/chart-colors.mjs && node test/content-cards.mjs && node test/chart-workbook.mjs && node test/single-column-chart.mjs && node --test test/chart-cache-import.mjs && node test/dependency-boundary.mjs && npm run build:browser-check && npm run test:styled-table && node test/content-layout.mjs && node test/layout-parity.mjs && node test/gallery-layout-alignment.mjs && node test/cover-centering.mjs && node test/table-frame-furniture-order.mjs && node test/media-placeholder.mjs && node test/shape-border-widths.mjs && node test/image-placeholder.mjs && node test/svg-image-degrade.mjs && node test/background-colorref.mjs && node test/import-fabrications.mjs && node test/list-shape-names.mjs && node test/shared-quote.mjs && node test/quote-provenance.mjs && npm run test:timeline && npm run test:determinism",
|
|
42
42
|
"validate": "node scripts/validate-package.mjs",
|
|
43
|
-
"test:packed": "node test/packed-install.mjs",
|
|
43
|
+
"test:packed": "node test/packed-install.mjs && node test/provenance-interop.mjs",
|
|
44
44
|
"test:compare-published": "node test/compare-published.mjs",
|
|
45
|
+
"test:published-import": "node test/provenance-interop.mjs",
|
|
45
46
|
"test:browser": "npm run build:browser-check && node test/browser-check.mjs",
|
|
46
47
|
"prepack": "npm run build",
|
|
47
48
|
"build:browser-check": "node scripts/build-browser-check.mjs",
|
|
@@ -60,7 +61,7 @@
|
|
|
60
61
|
"test:determinism": "node test/determinism.mjs"
|
|
61
62
|
},
|
|
62
63
|
"dependencies": {
|
|
63
|
-
"@openpresentation/opf": "^0.11.
|
|
64
|
+
"@openpresentation/opf": "^0.11.4",
|
|
64
65
|
"fast-xml-parser": "^5.8.0",
|
|
65
66
|
"fflate": "^0.8.3",
|
|
66
67
|
"jszip": "3.10.2",
|
|
@@ -76,7 +77,7 @@
|
|
|
76
77
|
},
|
|
77
78
|
"devDependencies": {
|
|
78
79
|
"@expo-google-fonts/carlito": "0.4.1",
|
|
79
|
-
"@openpresentation/opf-render": "0.11.
|
|
80
|
+
"@openpresentation/opf-render": "0.11.9",
|
|
80
81
|
"esbuild": "0.28.2",
|
|
81
82
|
"playwright": "1.63.0"
|
|
82
83
|
},
|