@openpresentation/opf-pptx 0.8.0 → 0.8.1
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 +8 -0
- package/dist/code-provenance.js +4 -3
- package/dist/furniture-provenance.js +214 -0
- package/dist/index.js +65 -22
- package/package.json +9 -8
package/README.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# OPF PPTX
|
|
2
2
|
|
|
3
|
+
Version 0.8.1 requires core 0.10.1, including metric, quote and timeline layout placeholders and the corrected text-bullet contract. It retains the existing PowerPoint export APIs.
|
|
4
|
+
|
|
5
|
+
Unfinished prepared shaping work is preserved in the [September 15 roadmap](docs/roadmap-shaping-20260915.md); it is not part of the published runtime.
|
|
6
|
+
|
|
3
7
|
Version 0.8.0 and this checkout require Node 24 (`24.x`). Use `.nvmrc` for local development. Earlier published versions retain their original engine declarations. Browser entrypoints remain browser-safe; native application compatibility is verified separately.
|
|
4
8
|
|
|
5
9
|
Version 0.8.0 consumes shared heading/scalar/rich line placement, including `textRasterPadding`, through editable native line boxes without autofit. Complete heading tags retain title/subtitle/tag roles, line ordering and current edited text during import. Source boundary metadata also records hard-line separators, so complete scalar/heading groups recover current native text with spaces, tabs and exact authored line endings. Incomplete, duplicate, ambiguous or bulleted groups fall back to ordinary import with diagnostics. Legacy heading tags retain their prior native-line behavior; arbitrary formatting, nesting and geometry are not reconstructed. See the [source contract and limits](https://github.com/OpenPresentation/opf/blob/f94125a1ff95bf0974a055fe1c081d348dad54f2/docs/plans/text-placement.md). Native raster certification remains separate.
|
|
@@ -232,3 +236,7 @@ Export consumes the same core geometry as SVG. It writes native merged cells and
|
|
|
232
236
|
Version 0.5.0 requires core 0.7.0 and renderer 0.5.0 for coordinated previews. `npm run test:styled-table` runs export/import regressions. For local browser verification, set `OPF_CORE_ROOT` to the core repository and `OPF_RENDER_ROOT` to the coordinated renderer worktree when running `npm run build:browser-check`. Serve the repository and open `/artifacts/native-styled-table-import/browser/index.html`. Omit those variables to exercise installed package dependencies.
|
|
233
237
|
|
|
234
238
|
Version 0.8.0 exports editable native lines with explicit four-space tab stops. Boundary tags contain no original source words, so native edits and deletions remain authoritative. XML-unrepresentable scalar controls reject with an actionable path rather than silently losing characters. Twenty wide/portrait measured/estimated export cases, current-text mutation controls and damaged-tag fallbacks pass; native Office acceptance remains separate. The coordinated core browser workflow also verifies editing, undo and exact reimport.
|
|
239
|
+
|
|
240
|
+
The unpublished furniture candidate exports core's accepted header/footer text and picture geometry. Dedicated `OPF_FURNITURE_V1` tags describe roles, line boundaries, inactive flags and global/local scope. A slide manifest represents empty or disabled definitions without adding a visible shape. Tags contain no original text, image bytes or alt text. Reimport reads those values from current native shapes; complete groups recover literal dates, section/organization metadata and page-number intent when the current number matches the current slide position.
|
|
241
|
+
|
|
242
|
+
Missing, duplicated or inconsistent furniture tags fall back to current native content with an `invalid-furniture-provenance` diagnostic. Conflicting organization or section values also fall back. Inherited definitions become global only when every slide has a valid definition/override and all inherited values agree; otherwise recovered definitions stay local. Reordering slides with unchanged visible page numbers preserves those numbers as ordinary text instead of silently renumbering them. Native typography, positioning, crop and unrelated metadata are not reconstructed; `furniture-import-reflow` requests review. `npm run test:furniture` exercises XML conversion and mutation controls. Native Office, visual corpus and clean installed-package acceptance remain separate gates; this behavior is not yet published.
|
package/dist/code-provenance.js
CHANGED
|
@@ -24,9 +24,9 @@ export function attachCodeTags(entries, records) {
|
|
|
24
24
|
return attachTextTags(entries,records,TAG,'opfCode','code');
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
export {unhex as decodeTextTag};
|
|
27
|
+
export {unhex as decodeTextTag, hex as encodeTextTag};
|
|
28
28
|
|
|
29
|
-
export function attachTextTags(entries, records, tagName, prefix, kind) {
|
|
29
|
+
export function attachTextTags(entries, records, tagName, prefix, kind, {pictures = false} = {}) {
|
|
30
30
|
if (!records.size) return;
|
|
31
31
|
let count = 0;
|
|
32
32
|
const seen = new Set(), types = [];
|
|
@@ -34,7 +34,8 @@ export function attachTextTags(entries, records, tagName, prefix, kind) {
|
|
|
34
34
|
const relPath = path.replace('/slides/','/slides/_rels/') + '.rels';
|
|
35
35
|
let rels = dec.decode(entries[relPath]);
|
|
36
36
|
const ids = new Set([...rels.matchAll(/\bId="([^"]+)"/g)].map(match=>match[1]));
|
|
37
|
-
const
|
|
37
|
+
const pattern = pictures ? /<p:(sp|pic)>[\s\S]*?<\/p:\1>/g : /<p:sp>[\s\S]*?<\/p:sp>/g;
|
|
38
|
+
const xml = dec.decode(entries[path]).replace(pattern, shape=>{
|
|
38
39
|
const name = shape.match(/<p:cNvPr\b[^>]*\bname="([^"]+)"/)?.[1];
|
|
39
40
|
if (!records.has(name)) return shape;
|
|
40
41
|
if (seen.has(name)) throw new Error(`Duplicate generated ${kind} shape.`);
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import {XMLParser} from 'fast-xml-parser';
|
|
2
|
+
import {attachTextTags, decodeTextTag, encodeTextTag} from './code-provenance.js';
|
|
3
|
+
import {sourceLineParagraphs} from './text-provenance.js';
|
|
4
|
+
|
|
5
|
+
const TAG = 'OPF_FURNITURE_V1';
|
|
6
|
+
const REL = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/tags';
|
|
7
|
+
const NS = 'http://schemas.openxmlformats.org/presentationml/2006/main';
|
|
8
|
+
const enc = new TextEncoder(), dec = new TextDecoder('utf-8', {fatal: true});
|
|
9
|
+
const parser = new XMLParser({ignoreAttributes: false, attributeNamePrefix: '', parseTagValue: false, trimValues: false});
|
|
10
|
+
const array = value => value === undefined ? [] : Array.isArray(value) ? value : [value];
|
|
11
|
+
const kinds = ['header', 'footer'], zones = ['left', 'center', 'right'];
|
|
12
|
+
const fields = ['text', 'image', 'organization', 'section', 'slideNumber', 'date'];
|
|
13
|
+
const object = value => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
14
|
+
const key = part => `${part.kind}.${part.zone}.${part.field}`;
|
|
15
|
+
const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
16
|
+
const check = (condition, message) => { if (!condition) throw Error(message); };
|
|
17
|
+
|
|
18
|
+
// This manifest contains topology, identities and inactive flags only. Current
|
|
19
|
+
// native shapes supply all words, image bytes and alt text, even after edits.
|
|
20
|
+
export function furnitureManifest(presentation, slide, layout, slideIndex) {
|
|
21
|
+
const definitions = {};
|
|
22
|
+
for (const kind of kinds) {
|
|
23
|
+
const local = slide.design?.[kind] !== undefined;
|
|
24
|
+
const source = local ? slide.design[kind] : presentation.design?.[kind];
|
|
25
|
+
if (source === undefined) continue;
|
|
26
|
+
const value = source === false ? false : {};
|
|
27
|
+
if (value !== false) for (const zone of zones) if (source[zone] !== undefined) {
|
|
28
|
+
value[zone] = {};
|
|
29
|
+
for (const field of fields) if (source[zone][field] !== undefined) {
|
|
30
|
+
const current = source[zone][field];
|
|
31
|
+
value[zone][field] = field === 'image' ? 'native' : typeof current === 'string' ? 'literal' : current;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
definitions[kind] = {scope: local ? 'local' : 'global', value};
|
|
35
|
+
}
|
|
36
|
+
if (!Object.keys(definitions).length) return null;
|
|
37
|
+
const organizations = array(presentation.organization);
|
|
38
|
+
const organization = organizations.find(item => item.role === 'primary') ?? organizations[0];
|
|
39
|
+
return {v: 1, role: 'slide', group: String(slideIndex), definitions,
|
|
40
|
+
...(layout.parts.some(part => part.field === 'organization') ? {organizationId: organization?.id} : {}),
|
|
41
|
+
parts: layout.parts.map(part => ({kind: part.kind, zone: part.zone, field: part.field,
|
|
42
|
+
type: part.type, count: part.type === 'image' ? 1 : part.fit.lines.length}))};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function attachFurnitureTags(entries, records, manifests) {
|
|
46
|
+
attachTextTags(entries, records, TAG, 'opfFurniture', 'furniture', {pictures: true});
|
|
47
|
+
const types = [];
|
|
48
|
+
for (const [path, manifest] of manifests) {
|
|
49
|
+
const part = `ppt/tags/opfFurnitureSlide${manifest.group}.xml`;
|
|
50
|
+
const relPath = path.replace('/slides/', '/slides/_rels/') + '.rels';
|
|
51
|
+
let xml = dec.decode(entries[path]), rels = dec.decode(entries[relPath]);
|
|
52
|
+
// Common-slide customer data follows spTree, before controls/extLst.
|
|
53
|
+
// No invisible/fake shape is necessary for false or empty definitions.
|
|
54
|
+
const ids = new Set([...rels.matchAll(/\bId="([^"]+)"/g)].map(match => match[1]));
|
|
55
|
+
let id = 'rIdOpfFurnitureSlide';
|
|
56
|
+
while (ids.has(id)) id += '_';
|
|
57
|
+
check(!entries[part] && xml.includes('</p:spTree>'), 'Missing slide tree or colliding furniture tag part.');
|
|
58
|
+
xml = xml.replace('</p:spTree>', `</p:spTree><p:custDataLst><p:tags r:id="${id}"/></p:custDataLst>`);
|
|
59
|
+
rels = rels.replace('</Relationships>', `<Relationship Id="${id}" Type="${REL}" Target="../tags/opfFurnitureSlide${manifest.group}.xml"/></Relationships>`);
|
|
60
|
+
entries[part] = enc.encode(`<?xml version="1.0" encoding="UTF-8" standalone="yes"?><p:tagLst xmlns:p="${NS}"><p:tag name="${TAG}" val="${encodeTextTag(manifest)}"/></p:tagLst>`);
|
|
61
|
+
entries[path] = enc.encode(xml);
|
|
62
|
+
entries[relPath] = enc.encode(rels);
|
|
63
|
+
types.push(`<Override PartName="/${part}" ContentType="application/vnd.openxmlformats-officedocument.presentationml.tags+xml"/>`);
|
|
64
|
+
}
|
|
65
|
+
if (types.length) entries['[Content_Types].xml'] = enc.encode(dec.decode(entries['[Content_Types].xml']).replace('</Types>', types.join('') + '</Types>'));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function readTags(container, relationships, entries) {
|
|
69
|
+
const tags = [];
|
|
70
|
+
let unreadable = false;
|
|
71
|
+
for (const link of array(container?.['p:tags'])) {
|
|
72
|
+
const rel = relationships.get(link['r:id']);
|
|
73
|
+
try {
|
|
74
|
+
check(rel?.type === REL && rel.targetMode !== 'External' && entries[rel.path], 'Missing tag relationship.');
|
|
75
|
+
tags.push(...array(parser.parse(dec.decode(entries[rel.path]))['p:tagLst']?.['p:tag']));
|
|
76
|
+
} catch { unreadable = true; }
|
|
77
|
+
}
|
|
78
|
+
return {tags: tags.filter(tag => tag.name?.toUpperCase() === TAG),
|
|
79
|
+
ambiguous: unreadable || tags.filter(tag => /^OPF_/i.test(tag.name)).length !== 1};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function validateManifest(manifest) {
|
|
83
|
+
check(manifest?.v === 1 && manifest.role === 'slide' && /^\d{1,10}$/.test(manifest.group), 'Invalid slide identity.');
|
|
84
|
+
check(object(manifest.definitions) && Object.keys(manifest.definitions).length > 0 && Object.keys(manifest.definitions).every(kind => kinds.includes(kind)), 'Invalid furniture definitions.');
|
|
85
|
+
check(Array.isArray(manifest.parts) && manifest.parts.length <= 36, 'Invalid furniture parts.');
|
|
86
|
+
const expected = new Map();
|
|
87
|
+
for (const [kind, definition] of Object.entries(manifest.definitions)) {
|
|
88
|
+
check(object(definition) && ['global', 'local'].includes(definition.scope), 'Invalid furniture scope.');
|
|
89
|
+
const value = definition.value;
|
|
90
|
+
check(value === false || object(value), 'Invalid furniture definition.');
|
|
91
|
+
if (value === false) continue;
|
|
92
|
+
check(Object.keys(value).every(zone => zones.includes(zone)), 'Invalid furniture zone.');
|
|
93
|
+
for (const [zone, content] of Object.entries(value)) {
|
|
94
|
+
check(object(content) && Object.keys(content).every(field => fields.includes(field)), 'Invalid furniture fields.');
|
|
95
|
+
for (const [field, flag] of Object.entries(content)) {
|
|
96
|
+
check(field === 'image' ? flag === 'native' : field === 'text' ? flag === 'literal' :
|
|
97
|
+
field === 'date' ? flag === false || flag === 'literal' : typeof flag === 'boolean', 'Invalid furniture field intent.');
|
|
98
|
+
if (flag !== false) expected.set(`${kind}.${zone}.${field}`, field === 'image' ? 'image' : 'text');
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
check(manifest.parts.length === expected.size, 'Incomplete furniture topology.');
|
|
103
|
+
for (const part of manifest.parts) {
|
|
104
|
+
check(object(part) && expected.get(key(part)) === part.type, 'Ambiguous furniture part.');
|
|
105
|
+
check(Number.isSafeInteger(part.count) && part.count >= 1 && part.count <= 10000 && (part.type !== 'image' || part.count === 1), 'Invalid furniture line count.');
|
|
106
|
+
expected.delete(key(part));
|
|
107
|
+
}
|
|
108
|
+
if (manifest.parts.some(part => part.field === 'organization')) check(typeof manifest.organizationId === 'string' && /^[a-zA-Z0-9_-]+$/.test(manifest.organizationId), 'Invalid organization identity.');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function readSlide(context, entries, slideIndex, report, taggedText) {
|
|
112
|
+
const {root, shapes, paragraphs, pictures, relationships, readPicture} = context;
|
|
113
|
+
const candidates = {};
|
|
114
|
+
const records = [];
|
|
115
|
+
let invalidRecord = false;
|
|
116
|
+
for (const [type, nodes, properties] of [['text', shapes, 'p:nvSpPr'], ['image', pictures, 'p:nvPicPr']]) {
|
|
117
|
+
for (const [index, node] of nodes.entries()) {
|
|
118
|
+
const result = readTags(node[properties]?.['p:nvPr']?.['p:custDataLst'], relationships, entries);
|
|
119
|
+
if (type === 'text' && result.tags.length) taggedText.add(index);
|
|
120
|
+
for (const tag of result.tags) try {
|
|
121
|
+
check(!result.ambiguous, 'Multiple identities on a furniture shape.');
|
|
122
|
+
records.push({type, index, data: decodeTextTag(tag.val)});
|
|
123
|
+
} catch { invalidRecord = true; }
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
const tags = readTags(root['p:cSld']?.['p:custDataLst'], relationships, entries);
|
|
127
|
+
if (!tags.tags.length && !records.length && !invalidRecord) return candidates;
|
|
128
|
+
let manifest;
|
|
129
|
+
try {
|
|
130
|
+
check(!tags.ambiguous && tags.tags.length === 1 && !invalidRecord, 'Missing or ambiguous furniture manifest/shape tags.');
|
|
131
|
+
manifest = decodeTextTag(tags.tags[0].val);
|
|
132
|
+
validateManifest(manifest);
|
|
133
|
+
for (const record of records) {
|
|
134
|
+
const data = record.data, part = manifest.parts[data?.part];
|
|
135
|
+
check(data?.v === 1 && data.group === manifest.group && Number.isSafeInteger(data.part) && part && data.role === record.type && part.type === record.type, 'Unmatched furniture identity.');
|
|
136
|
+
}
|
|
137
|
+
} catch (error) { report(slideIndex, error.message); return candidates; }
|
|
138
|
+
for (const kind of kinds) {
|
|
139
|
+
const definition = manifest.definitions[kind];
|
|
140
|
+
if (!definition) continue;
|
|
141
|
+
try {
|
|
142
|
+
const value = definition.value === false ? false : {};
|
|
143
|
+
if (value !== false) for (const zone of zones) if (definition.value[zone] !== undefined) {
|
|
144
|
+
value[zone] = Object.fromEntries(Object.entries(definition.value[zone]).filter(([, flag]) => flag === false));
|
|
145
|
+
}
|
|
146
|
+
const candidate = {scope: definition.scope, value, text: [], pictures: [], organizations: [], sections: []};
|
|
147
|
+
for (const [partIndex, part] of manifest.parts.entries()) {
|
|
148
|
+
if (part.kind !== kind) continue;
|
|
149
|
+
const group = records.filter(record => record.data.part === partIndex);
|
|
150
|
+
check(group.length === part.count, 'Incomplete or duplicated furniture part.');
|
|
151
|
+
if (part.type === 'image') {
|
|
152
|
+
const current = readPicture(pictures[group[0].index]);
|
|
153
|
+
check(current?.kind === 'image', 'Furniture image is no longer recoverable.');
|
|
154
|
+
value[part.zone].image = current.payload.image;
|
|
155
|
+
candidate.pictures.push(group[0].index);
|
|
156
|
+
} else {
|
|
157
|
+
const ordered = Array(part.count);
|
|
158
|
+
for (const record of group) {
|
|
159
|
+
const {line, count} = record.data;
|
|
160
|
+
check(count === part.count && Number.isSafeInteger(line) && line >= 0 && line < part.count && !ordered[line], 'Ambiguous furniture line.');
|
|
161
|
+
ordered[line] = record;
|
|
162
|
+
}
|
|
163
|
+
const text = sourceLineParagraphs(ordered, paragraphs)[0].text;
|
|
164
|
+
if (part.field === 'slideNumber') check(text === String(slideIndex + 1), 'Current slide number differs from its position; retain the visible number as ordinary text.');
|
|
165
|
+
if (part.field === 'section') candidate.sections.push(text);
|
|
166
|
+
if (part.field === 'organization') candidate.organizations.push({id: manifest.organizationId, name: text});
|
|
167
|
+
value[part.zone][part.field] = ['text', 'date'].includes(part.field) ? text : true;
|
|
168
|
+
candidate.text.push(...ordered.map(record => record.index));
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
check(new Set(candidate.text).size === candidate.text.length && new Set(candidate.pictures).size === candidate.pictures.length, 'Repeated furniture shape identity.');
|
|
172
|
+
candidates[kind] = candidate;
|
|
173
|
+
} catch (error) { report(slideIndex, `${kind}: ${error.message}`); }
|
|
174
|
+
}
|
|
175
|
+
return candidates;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Reconcile metadata before consuming any shapes. Global inheritance is promoted
|
|
179
|
+
// only when every slide has a valid definition/override and all inherited values
|
|
180
|
+
// agree. A missing tag must never cause another slide's furniture to reappear.
|
|
181
|
+
export function importFurniture(contexts, entries, onDiagnostic) {
|
|
182
|
+
const report = (index, message) => onDiagnostic?.({code: 'invalid-furniture-provenance', path: `slides.${index}.design`, message: `${message} Ordinary import retains current native content; no old source words are restored.`});
|
|
183
|
+
const taggedText = contexts.map(() => new Set());
|
|
184
|
+
const candidates = contexts.map((context, index) => readSlide(context, entries, index, report, taggedText[index]));
|
|
185
|
+
const organizations = candidates.flatMap(slide => Object.values(slide).flatMap(candidate => candidate.organizations));
|
|
186
|
+
if (organizations.some(item => !same(item, organizations[0]))) {
|
|
187
|
+
for (const [index, slide] of candidates.entries()) for (const kind of kinds) if (slide[kind]?.organizations.length) {
|
|
188
|
+
delete slide[kind]; report(index, `${kind}: Current organization metadata disagrees across repeated fields.`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
for (const [index, slide] of candidates.entries()) {
|
|
192
|
+
const sections = Object.values(slide).flatMap(candidate => candidate.sections);
|
|
193
|
+
if (sections.some(section => section !== sections[0])) for (const kind of kinds) if (slide[kind]?.sections.length) {
|
|
194
|
+
delete slide[kind]; report(index, `${kind}: Current section metadata disagrees on this slide.`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
const result = {design: {}, slides: candidates.map((slide, index) => {
|
|
198
|
+
const values = Object.values(slide), sections = values.flatMap(candidate => candidate.sections);
|
|
199
|
+
return {design: Object.fromEntries(Object.entries(slide).map(([kind, candidate]) => [kind, candidate.value])),
|
|
200
|
+
...(sections.length ? {section: sections[0]} : {}),
|
|
201
|
+
text: new Set(values.flatMap(candidate => candidate.text)), pictures: new Set(values.flatMap(candidate => candidate.pictures)), taggedText: taggedText[index]};
|
|
202
|
+
})};
|
|
203
|
+
const validOrganizations = candidates.flatMap(slide => Object.values(slide).flatMap(candidate => candidate.organizations));
|
|
204
|
+
if (validOrganizations.length) result.organization = validOrganizations[0];
|
|
205
|
+
for (const kind of kinds) {
|
|
206
|
+
const inherited = candidates.map(slide => slide[kind]).filter(candidate => candidate?.scope === 'global');
|
|
207
|
+
if (inherited.length && candidates.every(slide => slide[kind]) && inherited.every(candidate => same(candidate.value, inherited[0].value))) {
|
|
208
|
+
result.design[kind] = inherited[0].value;
|
|
209
|
+
candidates.forEach((slide, index) => { if (slide[kind].scope === 'global') delete result.slides[index].design[kind]; });
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
candidates.forEach((slide, index) => { if (Object.keys(slide).length) onDiagnostic?.({code: 'furniture-import-reflow', path: `slides.${index}.design`, message: 'Complete furniture roles retain current text, source line boundaries, image content and unambiguous metadata. Native formatting, positioning, crop and other presentation metadata are not reconstructed; review the reflowed furniture.'}); });
|
|
213
|
+
return result;
|
|
214
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -6,6 +6,7 @@ import {attachCardTags,importCardFrames} from './card-provenance.js';
|
|
|
6
6
|
import {attachHeadingTags,importHeadingGroups} from './heading-provenance.js';
|
|
7
7
|
import {attachPlainTextTags,importPlainTextGroups} from './text-provenance.js';
|
|
8
8
|
import {attachTimelineTags,timelineManifest,importTimelineGroups} from './timeline-provenance.js';
|
|
9
|
+
import {attachFurnitureTags, furnitureManifest, importFurniture} from './furniture-provenance.js';
|
|
9
10
|
import {importImageOrientation} from './image-import.js';
|
|
10
11
|
import {nativeBackgroundFill} from './background.js';
|
|
11
12
|
import {importBackground} from './background-import.js';
|
|
@@ -181,6 +182,8 @@ export async function toPptx(input, options = {}) {
|
|
|
181
182
|
context.headingTags = new Map();
|
|
182
183
|
context.plainTextTags = new Map();
|
|
183
184
|
context.timelineTags = new Map();
|
|
185
|
+
context.furnitureTags = new Map();
|
|
186
|
+
context.furnitureManifests = new Map();
|
|
184
187
|
context.codeTags = new Map();
|
|
185
188
|
context.metricTags = new Map();
|
|
186
189
|
context.chartHeadings = new Map();
|
|
@@ -233,8 +236,21 @@ export async function fromPptx(input, options = {}) {
|
|
|
233
236
|
if (core.author) imported.author = core.author;
|
|
234
237
|
if (dimensions) imported.design = { dimensions };
|
|
235
238
|
|
|
239
|
+
const furnitureContexts = slidePaths.map((slidePath, index) => {
|
|
240
|
+
const root = parseRequiredXml(entries, slidePath)['p:sld'];
|
|
241
|
+
if (!root) throw new OPFPptxError('invalid-pptx', `PPTX slide is not a PresentationML slide: ${slidePath}.`, {path: slidePath});
|
|
242
|
+
const tree = root['p:cSld']?.['p:spTree'], relationships = parseRelationships(entries, slidePath);
|
|
243
|
+
return {root, relationships, shapes: nativeTextShapes(tree), pictures: nativePictures(tree),
|
|
244
|
+
paragraphs: nativeShapeParagraphs(decodeText(entries[slidePath])),
|
|
245
|
+
readPicture: picture => importPicture(entries, picture, slidePath, relationships,
|
|
246
|
+
diagnostic => options.onDiagnostic?.({...diagnostic, path: `slides.${index}.design`}))};
|
|
247
|
+
});
|
|
248
|
+
const furniture = importFurniture(furnitureContexts, entries, options.onDiagnostic);
|
|
249
|
+
if (Object.keys(furniture.design).length) imported.design = {...imported.design, ...furniture.design};
|
|
250
|
+
if (furniture.organization) imported.organization = furniture.organization;
|
|
251
|
+
|
|
236
252
|
for (let index = 0; index < slidePaths.length; index += 1) {
|
|
237
|
-
imported.slides.push(importSlide(entries, slidePaths[index], index, dimensions, options));
|
|
253
|
+
imported.slides.push(importSlide(entries, slidePaths[index], index, dimensions, options, furniture.slides[index], furnitureContexts[index]));
|
|
238
254
|
}
|
|
239
255
|
|
|
240
256
|
const result = validatePresentation(imported);
|
|
@@ -375,16 +391,9 @@ function dimensionsFromPresentation(presentationRoot) {
|
|
|
375
391
|
};
|
|
376
392
|
}
|
|
377
393
|
|
|
378
|
-
function importSlide(entries, slidePath, slideIndex, presentationDimensions, options) {
|
|
379
|
-
const
|
|
380
|
-
const
|
|
381
|
-
if (!slideRoot) {
|
|
382
|
-
throw new OPFPptxError("invalid-pptx", `PPTX slide is not a PresentationML slide: ${slidePath}.`, {
|
|
383
|
-
path: slidePath
|
|
384
|
-
});
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
const relationships = parseRelationships(entries, slidePath);
|
|
394
|
+
function importSlide(entries, slidePath, slideIndex, presentationDimensions, options, furniture, nativeContext) {
|
|
395
|
+
const slideRoot = nativeContext.root;
|
|
396
|
+
const relationships = nativeContext.relationships;
|
|
388
397
|
const dimensions = presentationDimensions ?? DIMENSION_PRESETS.widescreen;
|
|
389
398
|
const slide = {};
|
|
390
399
|
if (slideRoot.show === "0") slide.hidden = true;
|
|
@@ -393,8 +402,10 @@ function importSlide(entries, slidePath, slideIndex, presentationDimensions, opt
|
|
|
393
402
|
part: (path, parser) => parseRequiredXml(entries, path, parser), relationships: path => parseRelationships(entries, path), bytes: path => entries[path]
|
|
394
403
|
}, diagnostic => options.onDiagnostic?.({...diagnostic, path: `slides.${slideIndex}.design.background`}));
|
|
395
404
|
if (background) slide.design = {background};
|
|
405
|
+
if (Object.keys(furniture.design).length) slide.design = {...slide.design, ...furniture.design};
|
|
406
|
+
if (furniture.section !== undefined) slide.section = furniture.section;
|
|
396
407
|
|
|
397
|
-
const items = collectSlideItems(entries, slideRoot, slidePath, relationships, dimensions, options, slideIndex)
|
|
408
|
+
const items = collectSlideItems(entries, slideRoot, slidePath, relationships, dimensions, options, slideIndex, furniture, nativeContext)
|
|
398
409
|
.sort(comparePositionedItems);
|
|
399
410
|
// Complete OPF heading roles are authoritative. Nearby body lines must not
|
|
400
411
|
// fill an absent role by geometry; explicit native placeholders still apply.
|
|
@@ -418,12 +429,14 @@ function importSlide(entries, slidePath, slideIndex, presentationDimensions, opt
|
|
|
418
429
|
return slide;
|
|
419
430
|
}
|
|
420
431
|
|
|
421
|
-
|
|
432
|
+
const nativePictures = tree => [...asArray(tree?.['p:pic']), ...asArray(tree?.['p:grpSp']).flatMap(nativePictures)];
|
|
433
|
+
|
|
434
|
+
function collectSlideItems(entries, slideRoot, slidePath, relationships, dimensions, options, slideIndex, furniture, nativeContext) {
|
|
422
435
|
const tree = slideRoot["p:cSld"]?.["p:spTree"];
|
|
423
436
|
const items = [];
|
|
424
|
-
const shapes =
|
|
425
|
-
if (tree?.['p:grpSp']) options.onDiagnostic?.({code:'grouped-text-reflow',path:`slides.${slideIndex}`,message:'Grouped native text
|
|
426
|
-
const paragraphs =
|
|
437
|
+
const shapes = nativeContext.shapes;
|
|
438
|
+
if (tree?.['p:grpSp']) options.onDiagnostic?.({code:'grouped-text-reflow',path:`slides.${slideIndex}`,message:'Grouped native text and pictures are retained, but group transforms and unsupported group members are not reconstructed; review the reflowed OPF.'});
|
|
439
|
+
const paragraphs = nativeContext.paragraphs;
|
|
427
440
|
const code = importCodeGroups(shapes, paragraphs, relationships, entries, diagnostic => options.onDiagnostic?.({...diagnostic,path:`slides.${slideIndex}.code`}));
|
|
428
441
|
const metric = importMetricGroups(shapes, paragraphs, relationships, entries, diagnostic => options.onDiagnostic?.({...diagnostic,path:`slides.${slideIndex}.metric`}));
|
|
429
442
|
const cards = importCardFrames(shapes, paragraphs, relationships, entries, diagnostic => options.onDiagnostic?.({...diagnostic,path:`slides.${slideIndex}`}));
|
|
@@ -446,8 +459,12 @@ function collectSlideItems(entries, slideRoot, slidePath, relationships, dimensi
|
|
|
446
459
|
for (const item of code.items) items.push({kind:'code',bounds:shapeBounds(item.shape['p:spPr']?.['a:xfrm']),payload:item.payload});
|
|
447
460
|
for (const item of metric.items) items.push({kind:'metric',bounds:shapeBounds(item.shape['p:spPr']?.['a:xfrm']),payload:item.payload});
|
|
448
461
|
for (const [index,shape] of shapes.entries()) {
|
|
462
|
+
if (furniture.text.has(index)) continue;
|
|
449
463
|
if (code.consumed.has(shape)||metric.consumed.has(shape)||cards.has(shape)||headings.consumed.has(shape)||plainText.consumed.has(shape)||timelines.consumed.has(shape)) continue;
|
|
450
|
-
const item = importShape(shape, dimensions, paragraphs[index]);
|
|
464
|
+
const item = importShape(shape, dimensions, paragraphs[index], furniture.taggedText.has(index));
|
|
465
|
+
// A damaged/edited furniture group falls back to current native text,
|
|
466
|
+
// including cleared text boxes, without inventing a title or shape label.
|
|
467
|
+
if (item && furniture.taggedText.has(index)) item.sourceText = true;
|
|
451
468
|
if (item) items.push(item);
|
|
452
469
|
}
|
|
453
470
|
|
|
@@ -461,7 +478,8 @@ function collectSlideItems(entries, slideRoot, slidePath, relationships, dimensi
|
|
|
461
478
|
if (item) items.push(item);
|
|
462
479
|
}
|
|
463
480
|
|
|
464
|
-
for (const [index, picture] of
|
|
481
|
+
for (const [index, picture] of nativeContext.pictures.entries()) {
|
|
482
|
+
if (furniture.pictures.has(index)) continue;
|
|
465
483
|
const report = diagnostic => options.onDiagnostic?.({...diagnostic, path: `slides.${slideIndex}.pictures.${index}`});
|
|
466
484
|
const item = importPicture(entries, picture, slidePath, relationships, report);
|
|
467
485
|
if (item) items.push(item);
|
|
@@ -538,7 +556,7 @@ function importGraphicFrame(entries, frame, slidePath, relationships, importedTa
|
|
|
538
556
|
function importPicture(entries, picture, slidePath, relationships, report) {
|
|
539
557
|
const bounds = shapeBounds(picture["p:spPr"]?.["a:xfrm"]);
|
|
540
558
|
const name = scalarText(picture["p:nvPicPr"]?.["p:cNvPr"]?.name).trim();
|
|
541
|
-
const alt = scalarText(picture["p:nvPicPr"]?.["p:cNvPr"]?.descr)
|
|
559
|
+
const alt = scalarText(picture["p:nvPicPr"]?.["p:cNvPr"]?.descr);
|
|
542
560
|
const relId = picture["p:blipFill"]?.["a:blip"]?.["r:embed"];
|
|
543
561
|
const relationship = relationships.get(relId);
|
|
544
562
|
let bytes = relationship?.path ? entries[relationship.path] : null;
|
|
@@ -926,8 +944,9 @@ async function addSlide(pptx, presentation, opfSlide, slideIndex, context, optio
|
|
|
926
944
|
const { widthInches, heightInches } = slideContext.dimensions;
|
|
927
945
|
const layout = resolveCatalogRecord(presentation, "layouts", opfSlide.layout, "blank") ?? {};
|
|
928
946
|
if (opfSlide.layout && layout.id !== opfSlide.layout) throw new OPFPptxError("catalog-resolution-failed", `Layout '${opfSlide.layout}' needs an inline or bundled catalog record.`, { path: `slides.${slideIndex}.layout` });
|
|
929
|
-
const geometry = composeSlide(opfSlide, { width: widthInches * 96, height: heightInches * 96, layout, slideIndex, fonts: slideContext.fonts, contentAlignment:opfSlide.design?.contentAlignment??presentation.design?.contentAlignment, titleAlignment:opfSlide.design?.titleAlignment??presentation.design?.titleAlignment, textRasterPadding:options.textRasterPadding, contentBox:opfSlide.design?.contentBox??presentation.design?.contentBox, textMeasurement: options.textMeasurement });
|
|
947
|
+
const geometry = composeSlide(opfSlide, { width: widthInches * 96, height: heightInches * 96, layout, presentation, slideIndex, fonts: slideContext.fonts, contentAlignment:opfSlide.design?.contentAlignment??presentation.design?.contentAlignment, titleAlignment:opfSlide.design?.titleAlignment??presentation.design?.titleAlignment, textRasterPadding:options.textRasterPadding, contentBox:opfSlide.design?.contentBox??presentation.design?.contentBox, textMeasurement: options.textMeasurement });
|
|
930
948
|
for (const diagnostic of geometry.diagnostics) options.onDiagnostic?.(diagnostic);
|
|
949
|
+
await addFurniture(slide,presentation,opfSlide,geometry.furniture,slideContext,options,slideIndex);
|
|
931
950
|
for (const item of geometry.items) {
|
|
932
951
|
const region = { x: item.box.x / 96, y: item.box.y / 96, w: item.box.width / 96, h: item.box.height / 96 };
|
|
933
952
|
if (item.frameBox) {
|
|
@@ -1128,6 +1147,7 @@ async function addImagePayload(slide, presentation, asset, region, path, context
|
|
|
1128
1147
|
h: region.h,
|
|
1129
1148
|
altText: assetAlt(asset, presentation)
|
|
1130
1149
|
});
|
|
1150
|
+
return objectName;
|
|
1131
1151
|
}
|
|
1132
1152
|
|
|
1133
1153
|
function addChartPayload(slide, chart, region, context) {
|
|
@@ -1277,12 +1297,13 @@ function addMeasuredPayloadText(slide, text, box, context, options, config) {
|
|
|
1277
1297
|
if (context.composition?.overflow === 'error') throw new OPFPptxError('layout-overflow', diagnostic.message, {path: config.path, issues: [diagnostic]});
|
|
1278
1298
|
}
|
|
1279
1299
|
for (const [index, line] of fit.lines.entries()) {
|
|
1280
|
-
if (!line&&!config.heading&&!config.sourceText&&!config.timeline) continue;
|
|
1300
|
+
if (!line&&!config.heading&&!config.sourceText&&!config.timeline&&!config.keepEmpty) continue;
|
|
1281
1301
|
const alignment=fit.placement?.alignment??config.align??context.contentAlignment??'left',placed=fit.placement?.lines[index],factor=alignment==='right'?1:alignment==='center'?.5:0;
|
|
1282
1302
|
const sourceLine=fit.sourceLines?.[index],boundary=sourceLine?{boundary:sourceLine.boundary,separator:String(text).slice(sourceLine.end,sourceLine.nextStart)}:{};
|
|
1283
|
-
const objectName=config.heading?`OPF heading ${config.path} line ${index}`:config.timeline?`OPF timeline ${config.timeline.group} part ${config.timeline.part} line ${index}`:config.sourceText?`OPF text ${config.path} line ${index}`:undefined;
|
|
1303
|
+
const objectName=config.heading?`OPF heading ${config.path} line ${index}`:config.timeline?`OPF timeline ${config.timeline.group} part ${config.timeline.part} line ${index}`:config.sourceText?`OPF text ${config.path} line ${index}`:config.objectName?`${config.objectName} line ${index}`:undefined;
|
|
1284
1304
|
if(config.heading)context.headingTags.set(objectName,{v:1,group:config.path,field:config.heading,line:index,count:fit.lines.length,...boundary});
|
|
1285
1305
|
else if(config.timeline)context.timelineTags.set(objectName,{v:1,role:'text',group:config.timeline.group,part:config.timeline.part,line:index,count:fit.lines.length,...boundary,...(config.timeline.part===0&&index===0?{anchor:config.timeline.anchor}:{})});
|
|
1306
|
+
else if(config.furniture)context.furnitureTags.set(objectName,{v:1,role:'text',...config.furniture,line:index,count:fit.lines.length,...boundary});
|
|
1286
1307
|
else if(config.sourceText)context.plainTextTags.set(objectName,{v:1,group:config.path,line:index,count:fit.lines.length,...boundary});
|
|
1287
1308
|
const area=placed?{x:(placed.x+placed.width*factor-box.width*factor)/96,y:(placed.baseline-fit.fontSize)/96,w:box.width/96,h:placed.height/96}:{x:box.x/96,y:(box.y+index*fit.lineHeight)/96,w:box.width/96,h:fit.lineHeight/96};
|
|
1288
1309
|
slide.addText(line, {
|
|
@@ -1296,6 +1317,27 @@ function addMeasuredPayloadText(slide, text, box, context, options, config) {
|
|
|
1296
1317
|
}
|
|
1297
1318
|
}
|
|
1298
1319
|
|
|
1320
|
+
async function addFurniture(slide,presentation,source,layout,context,options,slideIndex) {
|
|
1321
|
+
if(!layout){
|
|
1322
|
+
if(['header','footer'].some(kind=>(source.design?.[kind]??presentation.design?.[kind])))throw new OPFPptxError('missing-furniture-layout','Header/footer export requires coordinated core furniture geometry.',{path:`slides.${slideIndex}.design`});
|
|
1323
|
+
const manifest = furnitureManifest(presentation, source, {parts: []}, slideIndex);
|
|
1324
|
+
if (manifest) context.furnitureManifests.set(`ppt/slides/slide${slideIndex + 1}.xml`, manifest);
|
|
1325
|
+
return;
|
|
1326
|
+
}
|
|
1327
|
+
for(const [index,part]of layout.parts.entries()){
|
|
1328
|
+
if(part.type==='image'){
|
|
1329
|
+
const region={x:part.box.x/96,y:part.box.y/96,w:part.box.width/96,h:part.box.height/96};
|
|
1330
|
+
const objectName = await addImagePayload(slide,presentation,part.image,region,part.path,{...context,imageFill:'fit'},options);
|
|
1331
|
+
if (objectName) context.furnitureTags.set(objectName, {v:1, role:'image', group:String(slideIndex), part:index});
|
|
1332
|
+
}else{
|
|
1333
|
+
if(!part.fit?.sourceLines)throw new OPFPptxError('missing-furniture-layout','Repeated text requires accepted source lines from core.',{path:part.path});
|
|
1334
|
+
addMeasuredPayloadText(slide,part.text,part.box,context,options,{path:part.path,fit:part.fit,textStyle:part.style,align:part.alignment,diagnosticsHandled:true,color:context.colors.mutedText,keepEmpty:true,objectName:`OPF furniture ${slideIndex} part ${index}`,furniture:{group:String(slideIndex),part:index}});
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
const manifest = furnitureManifest(presentation, source, layout, slideIndex);
|
|
1338
|
+
if (manifest) context.furnitureManifests.set(`ppt/slides/slide${slideIndex + 1}.xml`, manifest);
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1299
1341
|
function addCodePayload(slide, value, layout, region, context, path) {
|
|
1300
1342
|
if (!layout) throw new OPFPptxError('missing-code-layout', 'Code export requires a coordinated core build with shared code geometry.', {path});
|
|
1301
1343
|
for (const part of layout.parts) {
|
|
@@ -1692,6 +1734,7 @@ async function normalizePptxZip(raw, context) {
|
|
|
1692
1734
|
attachHeadingTags(entries,context.headingTags);
|
|
1693
1735
|
attachPlainTextTags(entries,context.plainTextTags);
|
|
1694
1736
|
attachTimelineTags(entries,context.timelineTags);
|
|
1737
|
+
attachFurnitureTags(entries,context.furnitureTags,context.furnitureManifests);
|
|
1695
1738
|
for(const [part,bytes]of Object.entries(entries)){
|
|
1696
1739
|
if(!/^ppt\/slides\/slide\d+\.xml$/.test(part))continue;
|
|
1697
1740
|
const relationships=parseRelationships(entries,part);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openpresentation/opf-pptx",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.1",
|
|
4
4
|
"description": "Pure local OPF to PPTX export and PPTX to OPF import tooling.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -37,8 +37,8 @@
|
|
|
37
37
|
"sideEffects": false,
|
|
38
38
|
"scripts": {
|
|
39
39
|
"build": "node scripts/build.mjs",
|
|
40
|
-
"typecheck": "node --check src/index.js && node --check scripts/build.mjs && node --check scripts/validate-package.mjs && node --check test/smoke.mjs && node --check src/image-geometry.js && 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/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 test/shared-timeline.mjs",
|
|
41
|
-
"test": "npm run build && node test/plain-whitespace.mjs && node test/accepted-text.mjs && node test/heading-provenance.mjs && node test/chart-colors.mjs && node test/content-cards.mjs && node test/chart-workbook.mjs && node test/dependency-boundary.mjs && npm run build:browser-check && npm run test:styled-table && node test/content-layout.mjs && node test/shared-quote.mjs && npm run test:timeline",
|
|
40
|
+
"typecheck": "node --check src/index.js && node --check scripts/build.mjs && node --check scripts/validate-package.mjs && node --check test/smoke.mjs && node --check src/image-geometry.js && 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/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 test/shared-timeline.mjs && node --check test/shared-furniture.mjs && node --check src/furniture-provenance.js && node --check test/furniture-provenance.mjs",
|
|
41
|
+
"test": "npm run build && npm run test:furniture && node test/plain-whitespace.mjs && node test/accepted-text.mjs && node test/heading-provenance.mjs && node test/chart-colors.mjs && node test/content-cards.mjs && node test/chart-workbook.mjs && node test/dependency-boundary.mjs && npm run build:browser-check && npm run test:styled-table && node test/content-layout.mjs && node test/shared-quote.mjs && npm run test:timeline",
|
|
42
42
|
"validate": "node scripts/validate-package.mjs",
|
|
43
43
|
"test:packed": "node test/packed-install.mjs",
|
|
44
44
|
"test:compare-published": "node test/compare-published.mjs",
|
|
@@ -49,17 +49,18 @@
|
|
|
49
49
|
"test:code": "node test/shared-code.mjs && node test/code-provenance.mjs",
|
|
50
50
|
"test:metric": "node test/shared-metric.mjs && node test/metric-provenance.mjs",
|
|
51
51
|
"test:font-variants": "node test/font-variants.mjs",
|
|
52
|
-
"test:timeline": "node test/shared-timeline.mjs"
|
|
52
|
+
"test:timeline": "node test/shared-timeline.mjs",
|
|
53
|
+
"test:furniture": "node test/shared-furniture.mjs && node test/furniture-provenance.mjs"
|
|
53
54
|
},
|
|
54
55
|
"dependencies": {
|
|
55
|
-
"@openpresentation/opf": "^0.10.
|
|
56
|
+
"@openpresentation/opf": "^0.10.1",
|
|
56
57
|
"fast-xml-parser": "^5.8.0",
|
|
57
58
|
"fflate": "^0.8.3",
|
|
58
|
-
"jszip": "3.10.
|
|
59
|
+
"jszip": "3.10.2",
|
|
59
60
|
"sharp": "0.35.4"
|
|
60
61
|
},
|
|
61
62
|
"peerDependencies": {
|
|
62
|
-
"@openpresentation/opf-render": "^0.8.
|
|
63
|
+
"@openpresentation/opf-render": "^0.8.1"
|
|
63
64
|
},
|
|
64
65
|
"peerDependenciesMeta": {
|
|
65
66
|
"@openpresentation/opf-render": {
|
|
@@ -67,7 +68,7 @@
|
|
|
67
68
|
}
|
|
68
69
|
},
|
|
69
70
|
"devDependencies": {
|
|
70
|
-
"@openpresentation/opf-render": "0.8.
|
|
71
|
+
"@openpresentation/opf-render": "0.8.1",
|
|
71
72
|
"esbuild": "0.28.2",
|
|
72
73
|
"playwright": "1.63.0"
|
|
73
74
|
},
|