@openpresentation/opf-pptx 0.8.0 → 0.9.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/DEPENDENCY-NOTES.md +4 -0
- package/README.md +9 -1
- package/dist/code-provenance.js +4 -3
- package/dist/color-ref.js +97 -0
- package/dist/furniture-provenance.js +214 -0
- package/dist/index.js +104 -43
- package/package.json +9 -8
package/DEPENDENCY-NOTES.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# PptxGenJS runtime provenance
|
|
2
2
|
|
|
3
|
+
## Coordinated `@openpresentation/opf` pin
|
|
4
|
+
|
|
5
|
+
PPTX 0.9.0 depends on published [`@openpresentation/opf@^0.11.0`](https://www.npmjs.com/package/@openpresentation/opf) (reference layer: ColorRef, document `variables`, exported `resolveColorRef()`). CI checks out OPF [`b8a1faf`](https://github.com/OpenPresentation/opf/commit/b8a1faf24203635bd36b09ea19f44bcd53781f16) (`opf-v0.11.0`) for coordinated source linking. Named content colors resolve through core `resolveColorRef()`; eight-digit hex alpha stays a local export concern because core `normalizeHexColor` drops the alpha byte. Native `schemeClr` / theme `clrScheme` writes remain follow-up work.
|
|
6
|
+
|
|
3
7
|
OPF PPTX 0.5.1 ships the exact, unmodified PptxGenJS 4.0.1 ESM distribution in `vendor/pptxgenjs`, with its MIT license, upstream archive integrity and per-file SHA-256 hashes. Its actual JSZip dependency is declared directly. Ordinary npm installations therefore omit the unused image-size parser without requiring consumer overrides. This removes the affected dependency; it does not patch the parser. Published OPF PPTX 0.5.0 retains the older graph.
|
|
4
8
|
|
|
5
9
|
The inspected PptxGenJS 4.0.1 ESM/CommonJS distributions do not import `image-size`. The only sizing-module reference is in a commented, unused helper. OPF supplies image geometry itself. The image-fit follow-up reads bounded PNG/JPEG/GIF/WebP dimension headers and inline JPEG EXIF orientation; it does not load image-size or decode pixels.
|
package/README.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# OPF PPTX
|
|
2
2
|
|
|
3
|
+
Version 0.9.0 requires core 0.11.0, including ColorRef / `variables` on content colors. It hex-resolves named colors through core `resolveColorRef()` before PptxGenJS `srgbClr` export. Unrecognized run colors such as `color:'invalid'` still validate and fall back to the theme text color. Native DrawingML `schemeClr` and theme `clrScheme` writes, and native `p:hf` headers/footers, are not in this release. Import still flattens theme colors to hex. Metric, quote and timeline layout placeholders and the corrected text-bullet contract from 0.8.1 are retained.
|
|
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.
|
|
@@ -68,7 +72,7 @@ The first exporter keeps the public API stable while using `pptxgenjs` internall
|
|
|
68
72
|
|
|
69
73
|
- `Slide.title`, `Slide.subtitle`, and `Slide.tag` become editable text boxes, not PowerPoint master placeholders.
|
|
70
74
|
- Root payloads, `blocks[]`, and promoted region keys become editable slide objects in deterministic regions. Promoted keys use the OPF 3x3 region vocabulary (`top`, `middle`, `bottom`, `left`, `center`, `right`).
|
|
71
|
-
- Text, lists, metrics, quotes, timelines, code, tables, and inline-data charts are emitted as editable PowerPoint text, table, and chart objects.
|
|
75
|
+
- Text, lists, metrics, quotes, timelines, code, tables, and inline-data charts are emitted as editable PowerPoint text, table, and chart objects. Content ColorRef values (hex, scheme slots/roles, and `var:<id>`) flatten to `a:srgbClr` via core `resolveColorRef()`; this is hex-resolve, not native `schemeClr` / theme write.
|
|
72
76
|
- Image assets are embedded only when supplied as data URIs, local paths, or host-resolved bytes/paths. Remote asset URLs are never fetched by the runtime path.
|
|
73
77
|
- ZIP entries, generated chart/workbook part names, core-property timestamps, and nested chart workbook timestamps are normalized for reproducible bytes.
|
|
74
78
|
|
|
@@ -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,97 @@
|
|
|
1
|
+
// Export ColorRef through published core resolveColorRef(). Keep 8-digit hex
|
|
2
|
+
// alpha locally: normalizeHexColor() strips the AA byte, which PptxGenJS needs.
|
|
3
|
+
|
|
4
|
+
import { resolveColorRef as resolveCoreColorRef } from "@openpresentation/opf";
|
|
5
|
+
|
|
6
|
+
const UNRESOLVED = "";
|
|
7
|
+
|
|
8
|
+
function isPlainObject(value) {
|
|
9
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function withHash(value) {
|
|
13
|
+
if (typeof value !== "string") return undefined;
|
|
14
|
+
const trimmed = value.trim();
|
|
15
|
+
if (!trimmed) return undefined;
|
|
16
|
+
return trimmed.startsWith("#") ? trimmed : `#${trimmed}`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function expandShorthand(hex) {
|
|
20
|
+
const raw = hex.replace(/^#/, "");
|
|
21
|
+
if (/^[0-9a-f]{3}$/i.test(raw)) return raw.split("").map((char) => char + char).join("");
|
|
22
|
+
return raw;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Literal #RGB / #RRGGBB / #RRGGBBAA, including pptx-internal hex without '#'. */
|
|
26
|
+
function literalExportHex(entry) {
|
|
27
|
+
if (typeof entry !== "string") return undefined;
|
|
28
|
+
const hashed = withHash(entry.trim());
|
|
29
|
+
if (!hashed) return undefined;
|
|
30
|
+
if (/^#(?:[0-9a-f]{3}|[0-9a-f]{6}|[0-9a-f]{8})$/i.test(hashed)) {
|
|
31
|
+
return `#${expandShorthand(hashed).toUpperCase()}`;
|
|
32
|
+
}
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Pass document variables through; core accepts hex shorthand or `{ type: "color", value }`. */
|
|
37
|
+
export function resolveVariableColors(variables) {
|
|
38
|
+
return isPlainObject(variables) ? variables : {};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function coreRoles(ctx) {
|
|
42
|
+
const colors = ctx.colors ?? {};
|
|
43
|
+
return {
|
|
44
|
+
background: withHash(colors.background),
|
|
45
|
+
surface: withHash(colors.surface),
|
|
46
|
+
text: withHash(colors.text),
|
|
47
|
+
textSecondary: withHash(colors.mutedText),
|
|
48
|
+
accent: withHash(colors.accent),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function coreOptions(ctx, fallback = UNRESOLVED) {
|
|
53
|
+
return {
|
|
54
|
+
colorScheme: ctx.colorScheme ?? {},
|
|
55
|
+
roles: coreRoles(ctx),
|
|
56
|
+
variables: ctx.variables ?? {},
|
|
57
|
+
fallback,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Resolve a ColorRef or TextRun.color string to a #RRGGBB or #RRGGBBAA value.
|
|
63
|
+
* Unrecognized values return undefined so callers can apply the theme fallback.
|
|
64
|
+
*/
|
|
65
|
+
export function resolveColorRefValue(entry, ctx) {
|
|
66
|
+
if (entry === undefined || entry === null || entry === "") return undefined;
|
|
67
|
+
if (typeof entry !== "string") return undefined;
|
|
68
|
+
const trimmed = entry.trim();
|
|
69
|
+
const literal = literalExportHex(trimmed);
|
|
70
|
+
if (literal) return literal;
|
|
71
|
+
const resolved = resolveCoreColorRef(trimmed, coreOptions(ctx, UNRESOLVED));
|
|
72
|
+
return resolved || undefined;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function colorContext(context, fallback) {
|
|
76
|
+
return {
|
|
77
|
+
colorScheme: context.colorScheme,
|
|
78
|
+
colors: context.colors,
|
|
79
|
+
variables: context.variables ?? {},
|
|
80
|
+
fallback: fallback ?? context.colors?.text ?? "000000",
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** PptxGenJS export color: six or eight uppercase hex digits without '#'. */
|
|
85
|
+
export function resolveExportColor(entry, ctx) {
|
|
86
|
+
const fallback = ctx.fallback ?? ctx.colors?.text ?? "000000";
|
|
87
|
+
const resolved = resolveColorRefValue(entry, ctx);
|
|
88
|
+
const hex = resolved ?? withHash(fallback) ?? "#000000";
|
|
89
|
+
const raw = expandShorthand(hex.replace(/^#/, ""));
|
|
90
|
+
if (/^[0-9A-F]{6}([0-9A-F]{2})?$/i.test(raw)) return raw.toUpperCase();
|
|
91
|
+
return fallback.replace(/^#/, "").toUpperCase().slice(0, 6);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function exportColorAlpha(entry, ctx) {
|
|
95
|
+
const raw = resolveExportColor(entry, ctx);
|
|
96
|
+
return raw.length === 8 ? (1 - parseInt(raw.slice(6), 16) / 255) * 100 : 0;
|
|
97
|
+
}
|
|
@@ -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,12 +6,14 @@ 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';
|
|
12
13
|
import { webpToPng } from '#image-fallback';
|
|
13
14
|
import { rasterMetadata, pictureTransform, normalizeImageOrientation } from './image-geometry.js';
|
|
14
15
|
import { layoutTable, composeSlide, fitText, fitRichText, textWidthMeasurer, resolveCanvasDimensions, resolveFontFamilies, resolveTextStyle, textColorForFill, chartColorForFill } from "@openpresentation/opf/composition";
|
|
16
|
+
import { colorContext, resolveExportColor, resolveVariableColors } from "./color-ref.js";
|
|
15
17
|
import PptxGenJS from "../vendor/pptxgenjs/pptxgen.es.js";
|
|
16
18
|
import { unzipSync, zipSync } from "fflate";
|
|
17
19
|
import { XMLParser } from "fast-xml-parser";
|
|
@@ -181,6 +183,8 @@ export async function toPptx(input, options = {}) {
|
|
|
181
183
|
context.headingTags = new Map();
|
|
182
184
|
context.plainTextTags = new Map();
|
|
183
185
|
context.timelineTags = new Map();
|
|
186
|
+
context.furnitureTags = new Map();
|
|
187
|
+
context.furnitureManifests = new Map();
|
|
184
188
|
context.codeTags = new Map();
|
|
185
189
|
context.metricTags = new Map();
|
|
186
190
|
context.chartHeadings = new Map();
|
|
@@ -233,8 +237,21 @@ export async function fromPptx(input, options = {}) {
|
|
|
233
237
|
if (core.author) imported.author = core.author;
|
|
234
238
|
if (dimensions) imported.design = { dimensions };
|
|
235
239
|
|
|
240
|
+
const furnitureContexts = slidePaths.map((slidePath, index) => {
|
|
241
|
+
const root = parseRequiredXml(entries, slidePath)['p:sld'];
|
|
242
|
+
if (!root) throw new OPFPptxError('invalid-pptx', `PPTX slide is not a PresentationML slide: ${slidePath}.`, {path: slidePath});
|
|
243
|
+
const tree = root['p:cSld']?.['p:spTree'], relationships = parseRelationships(entries, slidePath);
|
|
244
|
+
return {root, relationships, shapes: nativeTextShapes(tree), pictures: nativePictures(tree),
|
|
245
|
+
paragraphs: nativeShapeParagraphs(decodeText(entries[slidePath])),
|
|
246
|
+
readPicture: picture => importPicture(entries, picture, slidePath, relationships,
|
|
247
|
+
diagnostic => options.onDiagnostic?.({...diagnostic, path: `slides.${index}.design`}))};
|
|
248
|
+
});
|
|
249
|
+
const furniture = importFurniture(furnitureContexts, entries, options.onDiagnostic);
|
|
250
|
+
if (Object.keys(furniture.design).length) imported.design = {...imported.design, ...furniture.design};
|
|
251
|
+
if (furniture.organization) imported.organization = furniture.organization;
|
|
252
|
+
|
|
236
253
|
for (let index = 0; index < slidePaths.length; index += 1) {
|
|
237
|
-
imported.slides.push(importSlide(entries, slidePaths[index], index, dimensions, options));
|
|
254
|
+
imported.slides.push(importSlide(entries, slidePaths[index], index, dimensions, options, furniture.slides[index], furnitureContexts[index]));
|
|
238
255
|
}
|
|
239
256
|
|
|
240
257
|
const result = validatePresentation(imported);
|
|
@@ -375,16 +392,9 @@ function dimensionsFromPresentation(presentationRoot) {
|
|
|
375
392
|
};
|
|
376
393
|
}
|
|
377
394
|
|
|
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);
|
|
395
|
+
function importSlide(entries, slidePath, slideIndex, presentationDimensions, options, furniture, nativeContext) {
|
|
396
|
+
const slideRoot = nativeContext.root;
|
|
397
|
+
const relationships = nativeContext.relationships;
|
|
388
398
|
const dimensions = presentationDimensions ?? DIMENSION_PRESETS.widescreen;
|
|
389
399
|
const slide = {};
|
|
390
400
|
if (slideRoot.show === "0") slide.hidden = true;
|
|
@@ -393,8 +403,10 @@ function importSlide(entries, slidePath, slideIndex, presentationDimensions, opt
|
|
|
393
403
|
part: (path, parser) => parseRequiredXml(entries, path, parser), relationships: path => parseRelationships(entries, path), bytes: path => entries[path]
|
|
394
404
|
}, diagnostic => options.onDiagnostic?.({...diagnostic, path: `slides.${slideIndex}.design.background`}));
|
|
395
405
|
if (background) slide.design = {background};
|
|
406
|
+
if (Object.keys(furniture.design).length) slide.design = {...slide.design, ...furniture.design};
|
|
407
|
+
if (furniture.section !== undefined) slide.section = furniture.section;
|
|
396
408
|
|
|
397
|
-
const items = collectSlideItems(entries, slideRoot, slidePath, relationships, dimensions, options, slideIndex)
|
|
409
|
+
const items = collectSlideItems(entries, slideRoot, slidePath, relationships, dimensions, options, slideIndex, furniture, nativeContext)
|
|
398
410
|
.sort(comparePositionedItems);
|
|
399
411
|
// Complete OPF heading roles are authoritative. Nearby body lines must not
|
|
400
412
|
// fill an absent role by geometry; explicit native placeholders still apply.
|
|
@@ -418,12 +430,14 @@ function importSlide(entries, slidePath, slideIndex, presentationDimensions, opt
|
|
|
418
430
|
return slide;
|
|
419
431
|
}
|
|
420
432
|
|
|
421
|
-
|
|
433
|
+
const nativePictures = tree => [...asArray(tree?.['p:pic']), ...asArray(tree?.['p:grpSp']).flatMap(nativePictures)];
|
|
434
|
+
|
|
435
|
+
function collectSlideItems(entries, slideRoot, slidePath, relationships, dimensions, options, slideIndex, furniture, nativeContext) {
|
|
422
436
|
const tree = slideRoot["p:cSld"]?.["p:spTree"];
|
|
423
437
|
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 =
|
|
438
|
+
const shapes = nativeContext.shapes;
|
|
439
|
+
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.'});
|
|
440
|
+
const paragraphs = nativeContext.paragraphs;
|
|
427
441
|
const code = importCodeGroups(shapes, paragraphs, relationships, entries, diagnostic => options.onDiagnostic?.({...diagnostic,path:`slides.${slideIndex}.code`}));
|
|
428
442
|
const metric = importMetricGroups(shapes, paragraphs, relationships, entries, diagnostic => options.onDiagnostic?.({...diagnostic,path:`slides.${slideIndex}.metric`}));
|
|
429
443
|
const cards = importCardFrames(shapes, paragraphs, relationships, entries, diagnostic => options.onDiagnostic?.({...diagnostic,path:`slides.${slideIndex}`}));
|
|
@@ -446,8 +460,12 @@ function collectSlideItems(entries, slideRoot, slidePath, relationships, dimensi
|
|
|
446
460
|
for (const item of code.items) items.push({kind:'code',bounds:shapeBounds(item.shape['p:spPr']?.['a:xfrm']),payload:item.payload});
|
|
447
461
|
for (const item of metric.items) items.push({kind:'metric',bounds:shapeBounds(item.shape['p:spPr']?.['a:xfrm']),payload:item.payload});
|
|
448
462
|
for (const [index,shape] of shapes.entries()) {
|
|
463
|
+
if (furniture.text.has(index)) continue;
|
|
449
464
|
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]);
|
|
465
|
+
const item = importShape(shape, dimensions, paragraphs[index], furniture.taggedText.has(index));
|
|
466
|
+
// A damaged/edited furniture group falls back to current native text,
|
|
467
|
+
// including cleared text boxes, without inventing a title or shape label.
|
|
468
|
+
if (item && furniture.taggedText.has(index)) item.sourceText = true;
|
|
451
469
|
if (item) items.push(item);
|
|
452
470
|
}
|
|
453
471
|
|
|
@@ -461,7 +479,8 @@ function collectSlideItems(entries, slideRoot, slidePath, relationships, dimensi
|
|
|
461
479
|
if (item) items.push(item);
|
|
462
480
|
}
|
|
463
481
|
|
|
464
|
-
for (const [index, picture] of
|
|
482
|
+
for (const [index, picture] of nativeContext.pictures.entries()) {
|
|
483
|
+
if (furniture.pictures.has(index)) continue;
|
|
465
484
|
const report = diagnostic => options.onDiagnostic?.({...diagnostic, path: `slides.${slideIndex}.pictures.${index}`});
|
|
466
485
|
const item = importPicture(entries, picture, slidePath, relationships, report);
|
|
467
486
|
if (item) items.push(item);
|
|
@@ -538,7 +557,7 @@ function importGraphicFrame(entries, frame, slidePath, relationships, importedTa
|
|
|
538
557
|
function importPicture(entries, picture, slidePath, relationships, report) {
|
|
539
558
|
const bounds = shapeBounds(picture["p:spPr"]?.["a:xfrm"]);
|
|
540
559
|
const name = scalarText(picture["p:nvPicPr"]?.["p:cNvPr"]?.name).trim();
|
|
541
|
-
const alt = scalarText(picture["p:nvPicPr"]?.["p:cNvPr"]?.descr)
|
|
560
|
+
const alt = scalarText(picture["p:nvPicPr"]?.["p:cNvPr"]?.descr);
|
|
542
561
|
const relId = picture["p:blipFill"]?.["a:blip"]?.["r:embed"];
|
|
543
562
|
const relationship = relationships.get(relId);
|
|
544
563
|
let bytes = relationship?.path ? entries[relationship.path] : null;
|
|
@@ -890,10 +909,15 @@ function resolvePresentationContext(presentation, options) {
|
|
|
890
909
|
accent: normalizeHex(colorScheme.primary ?? colorScheme.accent1 ?? "#2874A6"),
|
|
891
910
|
surface: normalizeHex(colorScheme.surface ?? (darkBackground ? colorScheme.dark2 : colorScheme.light2) ?? "#F8FAFC"),
|
|
892
911
|
border: normalizeHex(colorScheme.accent5 ?? "#CBD5E1")
|
|
893
|
-
}
|
|
912
|
+
},
|
|
913
|
+
variables: resolveVariableColors(presentation.variables)
|
|
894
914
|
};
|
|
895
915
|
}
|
|
896
916
|
|
|
917
|
+
function exportColor(entry, context, fallback) {
|
|
918
|
+
return resolveExportColor(entry, colorContext(context, fallback));
|
|
919
|
+
}
|
|
920
|
+
|
|
897
921
|
function configurePresentation(pptx, presentation, context) {
|
|
898
922
|
pptx.defineLayout({
|
|
899
923
|
name: context.layoutName,
|
|
@@ -926,8 +950,9 @@ async function addSlide(pptx, presentation, opfSlide, slideIndex, context, optio
|
|
|
926
950
|
const { widthInches, heightInches } = slideContext.dimensions;
|
|
927
951
|
const layout = resolveCatalogRecord(presentation, "layouts", opfSlide.layout, "blank") ?? {};
|
|
928
952
|
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 });
|
|
953
|
+
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
954
|
for (const diagnostic of geometry.diagnostics) options.onDiagnostic?.(diagnostic);
|
|
955
|
+
await addFurniture(slide,presentation,opfSlide,geometry.furniture,slideContext,options,slideIndex);
|
|
931
956
|
for (const item of geometry.items) {
|
|
932
957
|
const region = { x: item.box.x / 96, y: item.box.y / 96, w: item.box.width / 96, h: item.box.height / 96 };
|
|
933
958
|
if (item.frameBox) {
|
|
@@ -958,7 +983,10 @@ async function addSlide(pptx, presentation, opfSlide, slideIndex, context, optio
|
|
|
958
983
|
} else if (item.field === "text" && item.text?.richLines) {
|
|
959
984
|
const alignment=item.text.placement?.alignment??opfSlide.design?.contentAlignment??presentation.design?.contentAlignment??'left';
|
|
960
985
|
for(const [index,line] of item.text.richLines.entries()){
|
|
961
|
-
const runs=line.fragments.map(fragment=>
|
|
986
|
+
const runs=line.fragments.map(fragment=>{
|
|
987
|
+
const runColor=exportColor(fragment.run.color,slideContext,slideContext.colors.text);
|
|
988
|
+
return {text:fragment.text,options:{...nativeFontOptions(fragment.style),fontSize:fragment.fontSize*.75,color:normalizeHex(runColor),underline:fragment.run.underline?{color:normalizeHex(runColor)}:undefined,strike:fragment.run.strikethrough?'sngStrike':undefined,baseline:fragment.baselineShift?-fragment.baselineShift/fragment.fontSize*2000:undefined,hyperlink:fragment.run.link&&/^(https?:|mailto:)/i.test(fragment.run.link)?{url:fragment.run.link}:undefined}};
|
|
989
|
+
});
|
|
962
990
|
const placed=item.text.placement?.lines[index],factor=alignment==='right'?1:alignment==='center'?.5:0;
|
|
963
991
|
const area=placed?{...region,x:(placed.x+line.width*factor-item.box.width*factor)/96,y:placed.y/96,h:placed.height/96}:{...region,y:region.y+line.y/96,h:line.height/96};
|
|
964
992
|
if(runs.length)slide.addText(runs,{...textBoxOptions(area,slideContext,item.text.fontSize*.75),align:alignment,fit:'none',wrap:false,lineSpacingMultiple:1});
|
|
@@ -980,7 +1008,7 @@ function resolveSlideContext(presentation, slide, baseContext, options) {
|
|
|
980
1008
|
|| Math.abs(resolved.dimensions.heightInches - baseContext.dimensions.heightInches) > 1e-6) {
|
|
981
1009
|
throw new OPFPptxError("mixed-slide-dimensions", "PowerPoint requires one canvas size per presentation. Set dimensions on the deck or export this slide separately.");
|
|
982
1010
|
}
|
|
983
|
-
return { ...baseContext, backgroundDefinition: resolved.backgroundDefinition, colorScheme: resolved.colorScheme, fonts: resolved.fonts, colors: resolved.colors, imageFill: effective.design.imageFill ?? "fit" };
|
|
1011
|
+
return { ...baseContext, backgroundDefinition: resolved.backgroundDefinition, colorScheme: resolved.colorScheme, fonts: resolved.fonts, colors: resolved.colors, variables: resolved.variables, imageFill: effective.design.imageFill ?? "fit" };
|
|
984
1012
|
}
|
|
985
1013
|
|
|
986
1014
|
function fieldToType(field) {
|
|
@@ -1047,8 +1075,12 @@ function addTextPayload(slide, value, region, context) {
|
|
|
1047
1075
|
slide.addText(stringifyText(value), textBoxOptions(region, context, 18));
|
|
1048
1076
|
}
|
|
1049
1077
|
|
|
1050
|
-
function richLineRuns(line,color) {
|
|
1051
|
-
|
|
1078
|
+
function richLineRuns(line,color,context) {
|
|
1079
|
+
const fallback=color.replace(/^#/,'');
|
|
1080
|
+
return line.fragments.map(fragment=>{
|
|
1081
|
+
const runColor=exportColor(fragment.run.color,context,fallback);
|
|
1082
|
+
return {text:fragment.text,options:{...nativeFontOptions(fragment.style),fontSize:fragment.fontSize*.75,color:normalizeHex(runColor),underline:fragment.run.underline?{color:normalizeHex(runColor)}:undefined,strike:fragment.run.strikethrough?'sngStrike':undefined,baseline:fragment.baselineShift?-fragment.baselineShift/fragment.fontSize*2000:undefined,hyperlink:fragment.run.link&&/^(https?:|mailto:)/i.test(fragment.run.link)?{url:fragment.run.link}:undefined}};
|
|
1083
|
+
});
|
|
1052
1084
|
}
|
|
1053
1085
|
function addMeasuredList(slide,fit,context) {
|
|
1054
1086
|
for(const entry of fit.listEntries){
|
|
@@ -1059,7 +1091,7 @@ function addMeasuredList(slide,fit,context) {
|
|
|
1059
1091
|
const objectName=first?`OPF list paragraph ${context.listMarkers.size+1}`:undefined;
|
|
1060
1092
|
if(first)context.listMarkers.set(objectName,{fontFamily:entry.marker.style.fontFamily,fontSize:entry.marker.fontSize*.75,color:normalizeHex(context.colors.text)});
|
|
1061
1093
|
const paragraph=first?{bullet:{characterCode:entry.marker.text.codePointAt(0).toString(16).padStart(4,'0'),indent:entry.marker.indent*.75},indentLevel:level}:{bullet:false};
|
|
1062
|
-
const runs=richLineRuns(line,color);
|
|
1094
|
+
const runs=richLineRuns(line,color,context);
|
|
1063
1095
|
if(!runs.length)runs.push({text:'',options:{}});
|
|
1064
1096
|
// Keep paragraph intent identical across runs. ZIP normalization below
|
|
1065
1097
|
// removes the duplicate paragraph-property nodes emitted by PptxGenJS.
|
|
@@ -1128,6 +1160,7 @@ async function addImagePayload(slide, presentation, asset, region, path, context
|
|
|
1128
1160
|
h: region.h,
|
|
1129
1161
|
altText: assetAlt(asset, presentation)
|
|
1130
1162
|
});
|
|
1163
|
+
return objectName;
|
|
1131
1164
|
}
|
|
1132
1165
|
|
|
1133
1166
|
function addChartPayload(slide, chart, region, context) {
|
|
@@ -1190,8 +1223,10 @@ function addTablePayload(slide, table, region, context, options, path) {
|
|
|
1190
1223
|
const rows = layout.rows.map(row => row.cells.map(cell => {
|
|
1191
1224
|
const {header,rich,fit} = cell;
|
|
1192
1225
|
const cellStyle = cell.style ?? {};
|
|
1193
|
-
const
|
|
1194
|
-
const
|
|
1226
|
+
const defaultFillHex = header ? context.colors.accent : context.colors.surface;
|
|
1227
|
+
const baseFill = exportColor(cellStyle.fill ?? defaultFillHex, context, defaultFillHex);
|
|
1228
|
+
const inheritedText = textColorForFill(`#${baseFill}`, header ? "#FFFFFF" : `#${context.colors.text}`);
|
|
1229
|
+
const baseColor = exportColor(cellStyle.color ?? inheritedText.replace(/^#/, ""), context, inheritedText.replace(/^#/, ""));
|
|
1195
1230
|
const alpha = value => value.length === 8 ? (1 - parseInt(value.slice(6), 16) / 255) * 100 : 0;
|
|
1196
1231
|
const text = stringifyText(cell.value), style = cell.textStyle;
|
|
1197
1232
|
const fragments = rich ? fit.richLines.flatMap(line => line.fragments) : [];
|
|
@@ -1199,7 +1234,7 @@ function addTablePayload(slide, table, region, context, options, path) {
|
|
|
1199
1234
|
const run = typeof value === 'string' ? {text:value} : value;
|
|
1200
1235
|
const fragment = fragments.find(item => item.runIndex === index);
|
|
1201
1236
|
const runStyle = fragment?.style ?? resolveTextStyle({...style,fontFamily:run.fontFamily ?? style.fontFamily,fontWeight:run.bold === undefined ? style.fontWeight : run.bold ? 700 : 400,italic:run.italic ?? style.italic}, options.textMeasurement);
|
|
1202
|
-
const rawColor =
|
|
1237
|
+
const rawColor = run.color !== undefined && run.color !== '' ? exportColor(run.color, context, baseColor) : baseColor;
|
|
1203
1238
|
const color = normalizeHex(rawColor), transparency = rawColor.length === 8 ? (1 - parseInt(rawColor.slice(6), 16) / 255) * 100 : 0;
|
|
1204
1239
|
const runOptions = {
|
|
1205
1240
|
...nativeFontOptions(runStyle),fontSize:fragment ? fragment.fontSize * .75 : fit.fontSize * .75,
|
|
@@ -1277,12 +1312,13 @@ function addMeasuredPayloadText(slide, text, box, context, options, config) {
|
|
|
1277
1312
|
if (context.composition?.overflow === 'error') throw new OPFPptxError('layout-overflow', diagnostic.message, {path: config.path, issues: [diagnostic]});
|
|
1278
1313
|
}
|
|
1279
1314
|
for (const [index, line] of fit.lines.entries()) {
|
|
1280
|
-
if (!line&&!config.heading&&!config.sourceText&&!config.timeline) continue;
|
|
1315
|
+
if (!line&&!config.heading&&!config.sourceText&&!config.timeline&&!config.keepEmpty) continue;
|
|
1281
1316
|
const alignment=fit.placement?.alignment??config.align??context.contentAlignment??'left',placed=fit.placement?.lines[index],factor=alignment==='right'?1:alignment==='center'?.5:0;
|
|
1282
1317
|
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;
|
|
1318
|
+
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
1319
|
if(config.heading)context.headingTags.set(objectName,{v:1,group:config.path,field:config.heading,line:index,count:fit.lines.length,...boundary});
|
|
1285
1320
|
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}:{})});
|
|
1321
|
+
else if(config.furniture)context.furnitureTags.set(objectName,{v:1,role:'text',...config.furniture,line:index,count:fit.lines.length,...boundary});
|
|
1286
1322
|
else if(config.sourceText)context.plainTextTags.set(objectName,{v:1,group:config.path,line:index,count:fit.lines.length,...boundary});
|
|
1287
1323
|
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
1324
|
slide.addText(line, {
|
|
@@ -1296,6 +1332,27 @@ function addMeasuredPayloadText(slide, text, box, context, options, config) {
|
|
|
1296
1332
|
}
|
|
1297
1333
|
}
|
|
1298
1334
|
|
|
1335
|
+
async function addFurniture(slide,presentation,source,layout,context,options,slideIndex) {
|
|
1336
|
+
if(!layout){
|
|
1337
|
+
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`});
|
|
1338
|
+
const manifest = furnitureManifest(presentation, source, {parts: []}, slideIndex);
|
|
1339
|
+
if (manifest) context.furnitureManifests.set(`ppt/slides/slide${slideIndex + 1}.xml`, manifest);
|
|
1340
|
+
return;
|
|
1341
|
+
}
|
|
1342
|
+
for(const [index,part]of layout.parts.entries()){
|
|
1343
|
+
if(part.type==='image'){
|
|
1344
|
+
const region={x:part.box.x/96,y:part.box.y/96,w:part.box.width/96,h:part.box.height/96};
|
|
1345
|
+
const objectName = await addImagePayload(slide,presentation,part.image,region,part.path,{...context,imageFill:'fit'},options);
|
|
1346
|
+
if (objectName) context.furnitureTags.set(objectName, {v:1, role:'image', group:String(slideIndex), part:index});
|
|
1347
|
+
}else{
|
|
1348
|
+
if(!part.fit?.sourceLines)throw new OPFPptxError('missing-furniture-layout','Repeated text requires accepted source lines from core.',{path:part.path});
|
|
1349
|
+
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}});
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
const manifest = furnitureManifest(presentation, source, layout, slideIndex);
|
|
1353
|
+
if (manifest) context.furnitureManifests.set(`ppt/slides/slide${slideIndex + 1}.xml`, manifest);
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1299
1356
|
function addCodePayload(slide, value, layout, region, context, path) {
|
|
1300
1357
|
if (!layout) throw new OPFPptxError('missing-code-layout', 'Code export requires a coordinated core build with shared code geometry.', {path});
|
|
1301
1358
|
for (const part of layout.parts) {
|
|
@@ -1452,9 +1509,9 @@ function textRuns(value, context, fallbackFontSize) {
|
|
|
1452
1509
|
options: {
|
|
1453
1510
|
bold: run?.bold,
|
|
1454
1511
|
italic: run?.italic,
|
|
1455
|
-
underline: run?.underline ? { color: normalizeHex(run
|
|
1512
|
+
underline: run?.underline ? { color: normalizeHex(exportColor(run?.color, context, context.colors.text)) } : undefined,
|
|
1456
1513
|
strike: run?.strikethrough ? "sngStrike" : undefined,
|
|
1457
|
-
color: normalizeHex(run?.color
|
|
1514
|
+
color: normalizeHex(exportColor(run?.color, context, context.colors.text)),
|
|
1458
1515
|
fontFace: run?.fontFamily ?? context.fonts.body,
|
|
1459
1516
|
fontSize: run?.fontSize ?? fallbackFontSize,
|
|
1460
1517
|
superscript: run?.superscript,
|
|
@@ -1630,18 +1687,19 @@ function resolveDimensions(value) {
|
|
|
1630
1687
|
}
|
|
1631
1688
|
|
|
1632
1689
|
function resolveBackground(value, colorScheme) {
|
|
1690
|
+
const fallback = "FFFFFF";
|
|
1633
1691
|
if (typeof value === "string") {
|
|
1634
|
-
if (value.startsWith("#")) return normalizeHex(value);
|
|
1635
|
-
return normalizeHex(colorScheme[value] ?? colorScheme.background ?? colorScheme.light1 ?? "#FFFFFF");
|
|
1692
|
+
if (value.startsWith("#")) return normalizeHex(value, fallback);
|
|
1693
|
+
return normalizeHex(colorScheme[value] ?? colorScheme.background ?? colorScheme.light1 ?? "#FFFFFF", fallback);
|
|
1636
1694
|
}
|
|
1637
1695
|
if (isPlainObject(value)) {
|
|
1638
|
-
if (value.type === "solid" && value.color) return normalizeHex(value.color);
|
|
1696
|
+
if (value.type === "solid" && value.color) return normalizeHex(value.color, fallback);
|
|
1639
1697
|
if (value.type === "theme" && value.slot) {
|
|
1640
|
-
return normalizeHex(colorScheme[value.slot] ?? colorScheme.light1 ?? "#FFFFFF");
|
|
1698
|
+
return normalizeHex(colorScheme[value.slot] ?? colorScheme.light1 ?? "#FFFFFF", fallback);
|
|
1641
1699
|
}
|
|
1642
|
-
if (value.backgroundColor) return normalizeHex(value.backgroundColor);
|
|
1700
|
+
if (value.backgroundColor) return normalizeHex(value.backgroundColor, fallback);
|
|
1643
1701
|
}
|
|
1644
|
-
return normalizeHex(colorScheme.background ?? colorScheme.light1 ?? "#FFFFFF");
|
|
1702
|
+
return normalizeHex(colorScheme.background ?? colorScheme.light1 ?? "#FFFFFF", fallback);
|
|
1645
1703
|
}
|
|
1646
1704
|
|
|
1647
1705
|
function resolveFonts(fontScheme) {
|
|
@@ -1655,8 +1713,8 @@ function readableTextColor(background, colorScheme) {
|
|
|
1655
1713
|
: normalizeHex(colorScheme.text ?? colorScheme.dark1 ?? "#0F172A");
|
|
1656
1714
|
}
|
|
1657
1715
|
|
|
1658
|
-
function normalizeHex(value) {
|
|
1659
|
-
if (typeof value !== "string") return "
|
|
1716
|
+
function normalizeHex(value, fallback = "000000") {
|
|
1717
|
+
if (typeof value !== "string") return fallback.replace(/^#/, "").toUpperCase().slice(0, 6);
|
|
1660
1718
|
const raw = value.trim().replace(/^#/, "");
|
|
1661
1719
|
if (/^[0-9a-fA-F]{3}$/.test(raw)) {
|
|
1662
1720
|
return raw.split("").map((char) => char + char).join("").toUpperCase();
|
|
@@ -1664,7 +1722,7 @@ function normalizeHex(value) {
|
|
|
1664
1722
|
if (/^[0-9a-fA-F]{6,8}$/.test(raw)) {
|
|
1665
1723
|
return raw.slice(0, 6).toUpperCase();
|
|
1666
1724
|
}
|
|
1667
|
-
return
|
|
1725
|
+
return fallback.replace(/^#/, "").toUpperCase().slice(0, 6);
|
|
1668
1726
|
}
|
|
1669
1727
|
|
|
1670
1728
|
function isDarkHex(value) {
|
|
@@ -1692,6 +1750,7 @@ async function normalizePptxZip(raw, context) {
|
|
|
1692
1750
|
attachHeadingTags(entries,context.headingTags);
|
|
1693
1751
|
attachPlainTextTags(entries,context.plainTextTags);
|
|
1694
1752
|
attachTimelineTags(entries,context.timelineTags);
|
|
1753
|
+
attachFurnitureTags(entries,context.furnitureTags,context.furnitureManifests);
|
|
1695
1754
|
for(const [part,bytes]of Object.entries(entries)){
|
|
1696
1755
|
if(!/^ppt\/slides\/slide\d+\.xml$/.test(part))continue;
|
|
1697
1756
|
const relationships=parseRelationships(entries,part);
|
|
@@ -1852,7 +1911,9 @@ function normalizePartBytes(path, bytes, context, renameMaps, entries, imageMeta
|
|
|
1852
1911
|
for (const [edge, native] of [['left','lnL'],['right','lnR'],['top','lnT'],['bottom','lnB']]) {
|
|
1853
1912
|
const border = style.borders?.[edge];
|
|
1854
1913
|
if (!border) continue;
|
|
1855
|
-
const
|
|
1914
|
+
const borderHex = exportColor(border.color, context, context.colors.border);
|
|
1915
|
+
const borderOpacity = borderHex.length === 8 ? parseInt(borderHex.slice(6), 16) / 255 : 1;
|
|
1916
|
+
const fill = border.width === 0 ? '<a:noFill/>' : nativeBackgroundFill({type:'solid',color:'#' + borderHex.slice(0, 6), opacity: borderOpacity},{width:1,height:1}, context.colors.border);
|
|
1856
1917
|
const dash = {solid:'solid',dash:'dash',dot:'sysDot'}[border.dash ?? 'solid'];
|
|
1857
1918
|
const line = `<a:${native} w="${Math.round(border.width * table.scale * 9525)}" cap="flat" cmpd="sng" algn="ctr">${fill}<a:prstDash val="${dash}"/></a:${native}>`;
|
|
1858
1919
|
const existing = new RegExp(`<a:${native}\\b[^>]*>[\\s\\S]*?<\\/a:${native}>`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openpresentation/opf-pptx",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
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 src/color-ref.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/color-ref-export.mjs && 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.
|
|
56
|
+
"@openpresentation/opf": "^0.11.0",
|
|
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
|
},
|