@arcships/pptx-core 0.3.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/LICENSE +201 -0
- package/README.md +34 -0
- package/dist/browser.d.ts +146 -0
- package/dist/browser.js +15668 -0
- package/dist/chunk-YX5SL7A5.js +709 -0
- package/dist/index.d.ts +89 -0
- package/dist/index.js +26 -0
- package/dist/types-Dg1bRe8C.d.ts +222 -0
- package/package.json +56 -0
|
@@ -0,0 +1,709 @@
|
|
|
1
|
+
// src/types.ts
|
|
2
|
+
var PptxPreviewError = class extends Error {
|
|
3
|
+
code;
|
|
4
|
+
cause;
|
|
5
|
+
constructor(code, message, cause) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = "PptxPreviewError";
|
|
8
|
+
this.code = code;
|
|
9
|
+
this.cause = cause;
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
// src/playback/errors.ts
|
|
14
|
+
var PptxPlaybackError = class extends Error {
|
|
15
|
+
code;
|
|
16
|
+
slideIndex;
|
|
17
|
+
objectKey;
|
|
18
|
+
sourceNodeId;
|
|
19
|
+
cause;
|
|
20
|
+
constructor(code, message, details = {}) {
|
|
21
|
+
super(message);
|
|
22
|
+
this.name = "PptxPlaybackError";
|
|
23
|
+
this.code = code;
|
|
24
|
+
this.slideIndex = details.slideIndex;
|
|
25
|
+
this.objectKey = details.objectKey;
|
|
26
|
+
this.sourceNodeId = details.sourceNodeId;
|
|
27
|
+
this.cause = details.cause;
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// src/playback/capability.ts
|
|
32
|
+
function createPptxCapabilityReport(features = []) {
|
|
33
|
+
const records = Object.freeze([...features]);
|
|
34
|
+
return Object.freeze({
|
|
35
|
+
discovered: records.length,
|
|
36
|
+
strict: records.filter((item) => item.disposition === "strict").length,
|
|
37
|
+
approximate: records.filter((item) => item.disposition === "approximate").length,
|
|
38
|
+
static: records.filter((item) => item.disposition === "static").length,
|
|
39
|
+
unparsed: records.filter((item) => item.disposition === "unparsed").length,
|
|
40
|
+
features: records
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// src/playback/identity.ts
|
|
45
|
+
function assertKeyPart(value, name) {
|
|
46
|
+
const normalized = value.trim();
|
|
47
|
+
if (!normalized || /[|/]/u.test(normalized)) {
|
|
48
|
+
throw new TypeError(`${name} \u4E0D\u80FD\u4E3A\u7A7A\uFF0C\u4E5F\u4E0D\u80FD\u5305\u542B\u201C|\u201D\u6216\u201C/\u201D\u3002`);
|
|
49
|
+
}
|
|
50
|
+
return normalized;
|
|
51
|
+
}
|
|
52
|
+
function createPptxObjectKey(parts) {
|
|
53
|
+
const slidePath = parts.slidePath.trim().replace(/^\/+|\/+$/gu, "");
|
|
54
|
+
if (!slidePath || slidePath.includes("|")) throw new TypeError("slidePath \u65E0\u6548\u3002");
|
|
55
|
+
const shapeId = assertKeyPart(parts.shapeId, "shapeId");
|
|
56
|
+
const groupPath = (parts.groupPath ?? []).map((id) => `group:${assertKeyPart(id, "groupPath")}`);
|
|
57
|
+
return `${slidePath}|${parts.source}|${[...groupPath, `shape:${shapeId}`].join("/")}`;
|
|
58
|
+
}
|
|
59
|
+
function uniqueIdentityMap(objects, value) {
|
|
60
|
+
const result = /* @__PURE__ */ new Map();
|
|
61
|
+
const duplicates = /* @__PURE__ */ new Set();
|
|
62
|
+
for (const object of objects) {
|
|
63
|
+
const key = value(object)?.trim();
|
|
64
|
+
if (!key || duplicates.has(key)) continue;
|
|
65
|
+
if (result.has(key)) {
|
|
66
|
+
result.delete(key);
|
|
67
|
+
duplicates.add(key);
|
|
68
|
+
} else result.set(key, object);
|
|
69
|
+
}
|
|
70
|
+
return result;
|
|
71
|
+
}
|
|
72
|
+
function matchPptxMorphObjects(fromObjects, toObjects) {
|
|
73
|
+
const matches = [];
|
|
74
|
+
const usedFrom = /* @__PURE__ */ new Set();
|
|
75
|
+
const usedTo = /* @__PURE__ */ new Set();
|
|
76
|
+
const appendMatches = (method, from, to) => {
|
|
77
|
+
for (const [identity, fromObject] of from) {
|
|
78
|
+
const toObject = to.get(identity);
|
|
79
|
+
if (!toObject || usedFrom.has(fromObject.key) || usedTo.has(toObject.key)) continue;
|
|
80
|
+
usedFrom.add(fromObject.key);
|
|
81
|
+
usedTo.add(toObject.key);
|
|
82
|
+
matches.push(Object.freeze({
|
|
83
|
+
from: fromObject.key,
|
|
84
|
+
to: toObject.key,
|
|
85
|
+
method,
|
|
86
|
+
confidence: "strong",
|
|
87
|
+
score: 1,
|
|
88
|
+
unique: true
|
|
89
|
+
}));
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
appendMatches(
|
|
93
|
+
"explicit-name",
|
|
94
|
+
uniqueIdentityMap(fromObjects, (object) => object.explicitMorphName),
|
|
95
|
+
uniqueIdentityMap(toObjects, (object) => object.explicitMorphName)
|
|
96
|
+
);
|
|
97
|
+
appendMatches(
|
|
98
|
+
"creation-id",
|
|
99
|
+
uniqueIdentityMap(fromObjects, (object) => object.creationId?.toLowerCase()),
|
|
100
|
+
uniqueIdentityMap(toObjects, (object) => object.creationId?.toLowerCase())
|
|
101
|
+
);
|
|
102
|
+
return Object.freeze(matches);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// src/playback/time-tree.ts
|
|
106
|
+
function startCondition(node) {
|
|
107
|
+
return node.conditions.find((condition) => condition.source === "start" && condition.event !== "delay") ?? node.conditions.find((condition) => condition.source === "start");
|
|
108
|
+
}
|
|
109
|
+
function nodeTypeEvent(node) {
|
|
110
|
+
if (node.nodeType === "clickEffect") return "on-click";
|
|
111
|
+
if (node.nodeType === "withEffect") return "with-previous";
|
|
112
|
+
if (node.nodeType === "afterEffect") return "after-previous";
|
|
113
|
+
return void 0;
|
|
114
|
+
}
|
|
115
|
+
function effectiveDuration(node) {
|
|
116
|
+
if (node.durationMs === "indefinite" || node.repeatCount === "indefinite") return "indefinite";
|
|
117
|
+
const repeat = typeof node.repeatCount === "number" ? node.repeatCount : 1;
|
|
118
|
+
return node.durationMs * Math.max(repeat, 1) * (node.autoReverse ? 2 : 1);
|
|
119
|
+
}
|
|
120
|
+
function triggerForCondition(condition, event, clickBoundary) {
|
|
121
|
+
if (event === "on-click") return `click:${clickBoundary}`;
|
|
122
|
+
if (event === "on-shape-click" && condition?.targetObjectKey) return `shape:${condition.targetObjectKey}`;
|
|
123
|
+
if (event === "on-media-bookmark" && condition?.bookmarkName) return `bookmark:${condition.bookmarkName}`;
|
|
124
|
+
return void 0;
|
|
125
|
+
}
|
|
126
|
+
function executableTrigger(event) {
|
|
127
|
+
return [
|
|
128
|
+
"delay",
|
|
129
|
+
"on-click",
|
|
130
|
+
"with-previous",
|
|
131
|
+
"after-previous",
|
|
132
|
+
"on-shape-click",
|
|
133
|
+
"on-begin",
|
|
134
|
+
"on-end",
|
|
135
|
+
"on-media-bookmark"
|
|
136
|
+
].includes(event);
|
|
137
|
+
}
|
|
138
|
+
function subtreeHasExplicitInteractiveTrigger(slide, nodeId, visited = /* @__PURE__ */ new Set()) {
|
|
139
|
+
if (visited.has(nodeId)) return false;
|
|
140
|
+
visited.add(nodeId);
|
|
141
|
+
const node = slide.nodes[nodeId];
|
|
142
|
+
if (!node) return false;
|
|
143
|
+
const event = startCondition(node)?.event ?? nodeTypeEvent(node);
|
|
144
|
+
if (["on-click", "on-shape-click", "on-media-bookmark"].includes(event ?? "")) return true;
|
|
145
|
+
if (node.effect) return false;
|
|
146
|
+
return node.childIds.some((childId) => subtreeHasExplicitInteractiveTrigger(slide, childId, visited));
|
|
147
|
+
}
|
|
148
|
+
function isMainSequenceAdvancedByClick(node) {
|
|
149
|
+
return node.nodeType === "mainSeq" && node.conditions.some((condition) => condition.source === "next" && condition.event === "on-next");
|
|
150
|
+
}
|
|
151
|
+
function compilePptxSlideSchedule(slide) {
|
|
152
|
+
if (!slide.rootNodeId || !slide.nodes[slide.rootNodeId]) {
|
|
153
|
+
return Object.freeze({ groups: Object.freeze([]), clickBoundaryCount: 0, unsupportedNodeIds: Object.freeze([]) });
|
|
154
|
+
}
|
|
155
|
+
const scheduled = [];
|
|
156
|
+
const unsupported = /* @__PURE__ */ new Set();
|
|
157
|
+
const previousByTrigger = /* @__PURE__ */ new Map();
|
|
158
|
+
const scheduledByNode = /* @__PURE__ */ new Map();
|
|
159
|
+
let clickBoundary = 0;
|
|
160
|
+
let order = 0;
|
|
161
|
+
let lastTrigger = "auto";
|
|
162
|
+
const visit = (nodeId, inherited) => {
|
|
163
|
+
const node = slide.nodes[nodeId];
|
|
164
|
+
if (!node) {
|
|
165
|
+
unsupported.add(nodeId);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
const condition = startCondition(node);
|
|
169
|
+
const explicitEvent = condition?.event ?? nodeTypeEvent(node);
|
|
170
|
+
const event = explicitEvent ?? inherited.triggerEvent ?? "delay";
|
|
171
|
+
let context = inherited;
|
|
172
|
+
if (explicitEvent && !executableTrigger(explicitEvent) && !node.effect) {
|
|
173
|
+
unsupported.add(node.id);
|
|
174
|
+
context = { triggerEvent: explicitEvent, clickBoundary: inherited.clickBoundary };
|
|
175
|
+
} else if (explicitEvent === "on-click" && !node.effect) {
|
|
176
|
+
clickBoundary += 1;
|
|
177
|
+
context = { triggerKey: `click:${clickBoundary}`, triggerEvent: event, clickBoundary };
|
|
178
|
+
} else if ((explicitEvent === "on-shape-click" || explicitEvent === "on-media-bookmark") && !node.effect) {
|
|
179
|
+
const key = triggerForCondition(condition, event, clickBoundary);
|
|
180
|
+
if (key) context = { triggerKey: key, triggerEvent: event, clickBoundary };
|
|
181
|
+
else unsupported.add(node.id);
|
|
182
|
+
}
|
|
183
|
+
if (node.effect) {
|
|
184
|
+
let boundary = context.clickBoundary;
|
|
185
|
+
let triggerKey = context.triggerKey;
|
|
186
|
+
if (event === "on-click" && context.triggerEvent !== "on-click") {
|
|
187
|
+
clickBoundary += 1;
|
|
188
|
+
boundary = clickBoundary;
|
|
189
|
+
triggerKey = `click:${boundary}`;
|
|
190
|
+
} else if (event === "on-shape-click" || event === "on-media-bookmark") {
|
|
191
|
+
triggerKey = triggerForCondition(condition, event, boundary) ?? context.triggerKey;
|
|
192
|
+
} else if (event === "with-previous" || event === "after-previous" || event === "on-begin" || event === "on-end") {
|
|
193
|
+
triggerKey ??= lastTrigger;
|
|
194
|
+
} else triggerKey ??= "auto";
|
|
195
|
+
if (!triggerKey || !executableTrigger(event)) {
|
|
196
|
+
unsupported.add(node.id);
|
|
197
|
+
} else {
|
|
198
|
+
const previous = previousByTrigger.get(triggerKey);
|
|
199
|
+
const delay = Math.max(0, condition?.delayMs ?? node.delayMs);
|
|
200
|
+
let startMs = delay;
|
|
201
|
+
if (event === "with-previous" && previous) startMs = previous.startMs + delay;
|
|
202
|
+
else if (event === "after-previous" && previous) {
|
|
203
|
+
startMs = previous.endMs === "indefinite" ? previous.startMs : previous.endMs + delay;
|
|
204
|
+
} else if ((event === "on-begin" || event === "on-end") && condition?.targetNodeId) {
|
|
205
|
+
const referenced = scheduledByNode.get(condition.targetNodeId);
|
|
206
|
+
if (referenced) {
|
|
207
|
+
const referenceTime = event === "on-begin" || referenced.endMs === "indefinite" ? referenced.startMs : referenced.endMs;
|
|
208
|
+
startMs = referenceTime + delay;
|
|
209
|
+
} else unsupported.add(node.id);
|
|
210
|
+
} else if (event === "delay" && node.container === "sequence" && previous) {
|
|
211
|
+
startMs = previous.endMs === "indefinite" ? previous.startMs : previous.endMs + delay;
|
|
212
|
+
}
|
|
213
|
+
const duration = effectiveDuration(node);
|
|
214
|
+
const item = Object.freeze({
|
|
215
|
+
nodeId: node.id,
|
|
216
|
+
effect: node.effect,
|
|
217
|
+
triggerKey,
|
|
218
|
+
triggerEvent: event,
|
|
219
|
+
clickBoundary: boundary,
|
|
220
|
+
startMs,
|
|
221
|
+
endMs: duration === "indefinite" ? "indefinite" : startMs + duration,
|
|
222
|
+
order: order++
|
|
223
|
+
});
|
|
224
|
+
scheduled.push(item);
|
|
225
|
+
previousByTrigger.set(triggerKey, item);
|
|
226
|
+
scheduledByNode.set(node.id, item);
|
|
227
|
+
lastTrigger = triggerKey;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
if (isMainSequenceAdvancedByClick(node)) {
|
|
231
|
+
for (const childId of node.childIds) {
|
|
232
|
+
const child = slide.nodes[childId];
|
|
233
|
+
const childEvent = child ? startCondition(child)?.event ?? nodeTypeEvent(child) : void 0;
|
|
234
|
+
const followsPrevious = ["with-previous", "after-previous", "on-begin", "on-end"].includes(childEvent ?? "");
|
|
235
|
+
if (followsPrevious || subtreeHasExplicitInteractiveTrigger(slide, childId)) {
|
|
236
|
+
visit(childId, context);
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
clickBoundary += 1;
|
|
240
|
+
visit(childId, {
|
|
241
|
+
triggerKey: `click:${clickBoundary}`,
|
|
242
|
+
triggerEvent: "on-click",
|
|
243
|
+
clickBoundary
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
} else {
|
|
247
|
+
for (const childId of node.childIds) visit(childId, context);
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
visit(slide.rootNodeId, { triggerKey: "auto", triggerEvent: "delay", clickBoundary: 0 });
|
|
251
|
+
const groupMap = /* @__PURE__ */ new Map();
|
|
252
|
+
for (const effect of scheduled) {
|
|
253
|
+
const group = groupMap.get(effect.triggerKey) ?? [];
|
|
254
|
+
group.push(effect);
|
|
255
|
+
groupMap.set(effect.triggerKey, group);
|
|
256
|
+
}
|
|
257
|
+
const groups = [...groupMap].map(([key, effects]) => {
|
|
258
|
+
const indefinite = effects.some((effect) => effect.endMs === "indefinite");
|
|
259
|
+
return Object.freeze({
|
|
260
|
+
key,
|
|
261
|
+
clickBoundary: effects[0]?.clickBoundary ?? 0,
|
|
262
|
+
durationMs: indefinite ? "indefinite" : Math.max(0, ...effects.map((effect) => effect.endMs)),
|
|
263
|
+
effects: Object.freeze([...effects].sort((left, right) => left.startMs - right.startMs || left.order - right.order))
|
|
264
|
+
});
|
|
265
|
+
}).sort((left, right) => {
|
|
266
|
+
if (left.key === "auto") return -1;
|
|
267
|
+
if (right.key === "auto") return 1;
|
|
268
|
+
return left.clickBoundary - right.clickBoundary;
|
|
269
|
+
});
|
|
270
|
+
return Object.freeze({
|
|
271
|
+
groups: Object.freeze(groups),
|
|
272
|
+
clickBoundaryCount: clickBoundary,
|
|
273
|
+
unsupportedNodeIds: Object.freeze([...unsupported])
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// src/playback/track-compiler.ts
|
|
278
|
+
var defaultValues = {
|
|
279
|
+
display: true,
|
|
280
|
+
opacity: 1,
|
|
281
|
+
"translate-x": 0,
|
|
282
|
+
"translate-y": 0,
|
|
283
|
+
"scale-x": 1,
|
|
284
|
+
"scale-y": 1,
|
|
285
|
+
rotate: 0,
|
|
286
|
+
"clip-path": "none",
|
|
287
|
+
filter: "none",
|
|
288
|
+
"fill-color": "",
|
|
289
|
+
"line-color": "",
|
|
290
|
+
"text-color": "",
|
|
291
|
+
"media-playback": "stopped",
|
|
292
|
+
"media-time": 0,
|
|
293
|
+
"media-volume": 1
|
|
294
|
+
};
|
|
295
|
+
function initialValue(initialState, objectKey, property, paragraphRange) {
|
|
296
|
+
const target = paragraphRange ? `${objectKey}#paragraph:${paragraphRange.start}-${paragraphRange.end}` : objectKey;
|
|
297
|
+
return initialState[target]?.[property] ?? defaultValues[property];
|
|
298
|
+
}
|
|
299
|
+
function numberValue(effect, name, fallback) {
|
|
300
|
+
const value = effect.values[name];
|
|
301
|
+
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
302
|
+
}
|
|
303
|
+
function motionEnd(effect) {
|
|
304
|
+
const path = effect.motionPath;
|
|
305
|
+
if (!path || !/^[\s\d.,+\-mlehvz]+$/iu.test(path)) return void 0;
|
|
306
|
+
const numbers = [...path.matchAll(/[+-]?(?:\d+(?:\.\d+)?|\.\d+)/gu)].map((match) => Number(match[0]));
|
|
307
|
+
if (numbers.length < 2) return void 0;
|
|
308
|
+
const rawX = numbers[numbers.length - 2];
|
|
309
|
+
const rawY = numbers[numbers.length - 1];
|
|
310
|
+
return {
|
|
311
|
+
x: rawX * (Math.abs(rawX) <= 10 ? numberValue(effect, "motionScaleX", 1) : 1),
|
|
312
|
+
y: rawY * (Math.abs(rawY) <= 10 ? numberValue(effect, "motionScaleY", 1) : 1)
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
function easingValue(node) {
|
|
316
|
+
return node.acceleration > 0 || node.deceleration > 0 ? `pptx(${node.acceleration},${node.deceleration})` : void 0;
|
|
317
|
+
}
|
|
318
|
+
function interpolateValue(from, to, progress) {
|
|
319
|
+
return typeof from === "number" && typeof to === "number" ? from + (to - from) * progress : progress >= 1 ? to : from;
|
|
320
|
+
}
|
|
321
|
+
function expandRepeats(fragments, node) {
|
|
322
|
+
if (!fragments.length || node.durationMs === "indefinite" || node.durationMs <= 0) return [...fragments];
|
|
323
|
+
const repeat = node.repeatCount === "indefinite" ? "indefinite" : Math.max(node.repeatCount ?? 1, 1);
|
|
324
|
+
const repeats = repeat === "indefinite" ? 1 : Math.ceil(repeat);
|
|
325
|
+
const period = node.durationMs * (node.autoReverse ? 2 : 1);
|
|
326
|
+
const result = [];
|
|
327
|
+
const effectStartMs = Math.min(...fragments.map((fragment) => fragment.startMs));
|
|
328
|
+
for (const source of fragments) {
|
|
329
|
+
if (source.startMs === source.endMs || ["display", "media-playback", "media-time", "media-volume"].includes(source.property)) {
|
|
330
|
+
if (source.startMs === source.endMs && source.startMs === effectStartMs && node.durationMs > 0) {
|
|
331
|
+
result.push({
|
|
332
|
+
...source,
|
|
333
|
+
endMs: source.startMs + (repeat === "indefinite" ? period : repeat * period),
|
|
334
|
+
from: source.to,
|
|
335
|
+
repeatStartMs: repeat === "indefinite" ? source.startMs : void 0,
|
|
336
|
+
repeatPeriodMs: repeat === "indefinite" ? period : void 0,
|
|
337
|
+
repeatIndefinite: repeat === "indefinite" || void 0
|
|
338
|
+
});
|
|
339
|
+
} else result.push(source);
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
for (let cycle = 0; cycle < repeats; cycle += 1) {
|
|
343
|
+
const remaining = repeat === "indefinite" ? 1 : Math.min(1, repeat - cycle);
|
|
344
|
+
if (remaining <= 0) break;
|
|
345
|
+
const start = source.startMs + cycle * period;
|
|
346
|
+
const forwardEnd = start + node.durationMs * remaining;
|
|
347
|
+
const forwardTo = interpolateValue(source.from, source.to, remaining);
|
|
348
|
+
result.push({
|
|
349
|
+
...source,
|
|
350
|
+
startMs: start,
|
|
351
|
+
endMs: forwardEnd,
|
|
352
|
+
to: forwardTo,
|
|
353
|
+
repeatStartMs: repeat === "indefinite" ? source.startMs : void 0,
|
|
354
|
+
repeatPeriodMs: repeat === "indefinite" ? period : void 0,
|
|
355
|
+
repeatIndefinite: repeat === "indefinite" || void 0
|
|
356
|
+
});
|
|
357
|
+
if (node.autoReverse && remaining === 1) result.push({
|
|
358
|
+
...source,
|
|
359
|
+
startMs: forwardEnd,
|
|
360
|
+
endMs: forwardEnd + node.durationMs,
|
|
361
|
+
from: source.to,
|
|
362
|
+
to: source.from,
|
|
363
|
+
repeatStartMs: repeat === "indefinite" ? source.startMs : void 0,
|
|
364
|
+
repeatPeriodMs: repeat === "indefinite" ? period : void 0,
|
|
365
|
+
repeatIndefinite: repeat === "indefinite" || void 0
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
return result;
|
|
370
|
+
}
|
|
371
|
+
function fragmentsForEffect(scheduled, initialState, node) {
|
|
372
|
+
const effect = scheduled.effect;
|
|
373
|
+
const objectKey = effect.targetObjectKey;
|
|
374
|
+
if (!objectKey) return [];
|
|
375
|
+
const baseDuration = typeof node.durationMs === "number" ? node.durationMs : 0;
|
|
376
|
+
const baseEnd = scheduled.startMs + baseDuration;
|
|
377
|
+
const fragment = (property, from, to, startMs = scheduled.startMs, endMs = baseEnd) => ({
|
|
378
|
+
objectKey,
|
|
379
|
+
paragraphRange: effect.paragraphRange,
|
|
380
|
+
property,
|
|
381
|
+
initialValue: initialValue(initialState, objectKey, property, effect.paragraphRange),
|
|
382
|
+
startMs,
|
|
383
|
+
endMs,
|
|
384
|
+
from,
|
|
385
|
+
to,
|
|
386
|
+
sourceNodeId: scheduled.nodeId,
|
|
387
|
+
order: scheduled.order,
|
|
388
|
+
fill: node.fill,
|
|
389
|
+
easing: easingValue(node)
|
|
390
|
+
});
|
|
391
|
+
let fragments;
|
|
392
|
+
switch (effect.kind) {
|
|
393
|
+
case "appear":
|
|
394
|
+
fragments = [fragment("display", false, true, scheduled.startMs, scheduled.startMs)];
|
|
395
|
+
break;
|
|
396
|
+
case "disappear":
|
|
397
|
+
fragments = [fragment("display", true, false, scheduled.startMs, scheduled.startMs)];
|
|
398
|
+
break;
|
|
399
|
+
case "fade-in":
|
|
400
|
+
fragments = [
|
|
401
|
+
fragment("display", false, true, scheduled.startMs, scheduled.startMs),
|
|
402
|
+
fragment("opacity", 0, 1)
|
|
403
|
+
];
|
|
404
|
+
break;
|
|
405
|
+
case "fade-out":
|
|
406
|
+
fragments = [
|
|
407
|
+
fragment("opacity", 1, 0),
|
|
408
|
+
fragment("display", true, false, baseEnd, baseEnd)
|
|
409
|
+
];
|
|
410
|
+
break;
|
|
411
|
+
case "scale":
|
|
412
|
+
{
|
|
413
|
+
const fallback = effect.presetClass === "emph" ? 1 : 0;
|
|
414
|
+
const fromX = numberValue(effect, "scaleFromX", fallback);
|
|
415
|
+
const fromY = numberValue(effect, "scaleFromY", fallback);
|
|
416
|
+
fragments = [
|
|
417
|
+
fragment("scale-x", fromX, numberValue(effect, "scaleToX", fromX * numberValue(effect, "scaleByX", 1))),
|
|
418
|
+
fragment("scale-y", fromY, numberValue(effect, "scaleToY", fromY * numberValue(effect, "scaleByY", 1)))
|
|
419
|
+
];
|
|
420
|
+
}
|
|
421
|
+
break;
|
|
422
|
+
case "emphasis": {
|
|
423
|
+
const property = effect.values.emphasisProperty;
|
|
424
|
+
if (property === "opacity") {
|
|
425
|
+
const from = numberValue(effect, "emphasisFrom", initialValue(initialState, objectKey, "opacity", effect.paragraphRange));
|
|
426
|
+
fragments = [fragment("opacity", from, numberValue(effect, "emphasisTo", from))];
|
|
427
|
+
} else fragments = [];
|
|
428
|
+
break;
|
|
429
|
+
}
|
|
430
|
+
case "rotate":
|
|
431
|
+
fragments = [fragment("rotate", 0, numberValue(effect, "rotationBy", 0))];
|
|
432
|
+
break;
|
|
433
|
+
case "motion-path": {
|
|
434
|
+
const end = motionEnd(effect);
|
|
435
|
+
fragments = end ? [fragment("translate-x", 0, end.x), fragment("translate-y", 0, end.y)] : [];
|
|
436
|
+
break;
|
|
437
|
+
}
|
|
438
|
+
case "wipe": {
|
|
439
|
+
const direction = effect.filter?.match(/wipe\(([^)]+)\)/iu)?.[1]?.toLowerCase();
|
|
440
|
+
if (direction !== "left" && direction !== "right") {
|
|
441
|
+
fragments = [];
|
|
442
|
+
break;
|
|
443
|
+
}
|
|
444
|
+
const from = direction === "right" ? "inset(0 0 0 100%)" : "inset(0 100% 0 0)";
|
|
445
|
+
fragments = [fragment("clip-path", from, "inset(0 0 0 0)")];
|
|
446
|
+
break;
|
|
447
|
+
}
|
|
448
|
+
case "media-command":
|
|
449
|
+
fragments = [fragment("media-playback", "stopped", effect.command ?? "play")];
|
|
450
|
+
break;
|
|
451
|
+
default:
|
|
452
|
+
fragments = [];
|
|
453
|
+
}
|
|
454
|
+
return expandRepeats(fragments, node);
|
|
455
|
+
}
|
|
456
|
+
function trackKey(fragment) {
|
|
457
|
+
const range = fragment.paragraphRange;
|
|
458
|
+
return `${fragment.objectKey}|${range ? `${range.start}:${range.end}` : "object"}|${fragment.property}`;
|
|
459
|
+
}
|
|
460
|
+
function compileFragments(fragments) {
|
|
461
|
+
const groups = /* @__PURE__ */ new Map();
|
|
462
|
+
for (const fragment of fragments) {
|
|
463
|
+
const key = trackKey(fragment);
|
|
464
|
+
const list = groups.get(key) ?? [];
|
|
465
|
+
list.push(fragment);
|
|
466
|
+
groups.set(key, list);
|
|
467
|
+
}
|
|
468
|
+
return Object.freeze([...groups.values()].map((items) => {
|
|
469
|
+
const ordered = [...items].sort((left, right) => left.startMs - right.startMs || left.order - right.order);
|
|
470
|
+
const frames = /* @__PURE__ */ new Map();
|
|
471
|
+
for (const item of ordered) {
|
|
472
|
+
frames.set(item.startMs, Object.freeze({
|
|
473
|
+
timeMs: item.startMs,
|
|
474
|
+
value: item.from,
|
|
475
|
+
easing: item.easing,
|
|
476
|
+
sourceNodeId: item.sourceNodeId
|
|
477
|
+
}));
|
|
478
|
+
frames.set(item.endMs, Object.freeze({
|
|
479
|
+
timeMs: item.endMs,
|
|
480
|
+
value: item.to,
|
|
481
|
+
sourceNodeId: item.sourceNodeId
|
|
482
|
+
}));
|
|
483
|
+
}
|
|
484
|
+
const keyframes = Object.freeze([...frames.values()].sort((left, right) => left.timeMs - right.timeMs));
|
|
485
|
+
const first = ordered[0];
|
|
486
|
+
return Object.freeze({
|
|
487
|
+
objectKey: first.objectKey,
|
|
488
|
+
paragraphRange: first.paragraphRange,
|
|
489
|
+
property: first.property,
|
|
490
|
+
initialValue: first.initialValue,
|
|
491
|
+
keyframes,
|
|
492
|
+
segments: Object.freeze(ordered.map((item) => Object.freeze({
|
|
493
|
+
startMs: item.startMs,
|
|
494
|
+
endMs: item.endMs,
|
|
495
|
+
from: item.from,
|
|
496
|
+
to: item.to,
|
|
497
|
+
sourceNodeId: item.sourceNodeId,
|
|
498
|
+
order: item.order,
|
|
499
|
+
fill: item.fill,
|
|
500
|
+
easing: item.easing,
|
|
501
|
+
repeatStartMs: item.repeatStartMs,
|
|
502
|
+
repeatPeriodMs: item.repeatPeriodMs,
|
|
503
|
+
repeatIndefinite: item.repeatIndefinite
|
|
504
|
+
}))),
|
|
505
|
+
endTimeMs: ordered.some((item) => item.repeatIndefinite) ? "indefinite" : Math.max(0, ...ordered.map((item) => item.endMs)),
|
|
506
|
+
repeatStartMs: ordered.find((item) => item.repeatIndefinite)?.repeatStartMs,
|
|
507
|
+
repeatPeriodMs: ordered.find((item) => item.repeatIndefinite)?.repeatPeriodMs,
|
|
508
|
+
repeatIndefinite: ordered.some((item) => item.repeatIndefinite) || void 0
|
|
509
|
+
});
|
|
510
|
+
}));
|
|
511
|
+
}
|
|
512
|
+
function compilePptxSlideTracks(slide, initialState = Object.freeze({})) {
|
|
513
|
+
const schedule = compilePptxSlideSchedule(slide);
|
|
514
|
+
const preparedState = Object.fromEntries(
|
|
515
|
+
Object.entries(initialState).map(([key, value]) => [key, { ...value }])
|
|
516
|
+
);
|
|
517
|
+
const sourceState = Object.freeze(Object.fromEntries(
|
|
518
|
+
Object.entries(preparedState).map(([key, value]) => [key, Object.freeze({ ...value })])
|
|
519
|
+
));
|
|
520
|
+
const compilableNodeIds = /* @__PURE__ */ new Set();
|
|
521
|
+
for (const scheduled of schedule.groups.flatMap((group) => group.effects)) {
|
|
522
|
+
const node = slide.nodes[scheduled.nodeId];
|
|
523
|
+
if (node && fragmentsForEffect(scheduled, sourceState, node).length > 0) {
|
|
524
|
+
compilableNodeIds.add(scheduled.nodeId);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
for (const scheduled of schedule.groups.flatMap((group) => group.effects)) {
|
|
528
|
+
const effect = scheduled.effect;
|
|
529
|
+
const objectKey = effect.targetObjectKey;
|
|
530
|
+
if (!objectKey || effect.presetClass !== "entr" || !compilableNodeIds.has(scheduled.nodeId)) continue;
|
|
531
|
+
const target = effect.paragraphRange ? `${objectKey}#paragraph:${effect.paragraphRange.start}-${effect.paragraphRange.end}` : objectKey;
|
|
532
|
+
const state = preparedState[target] ?? {};
|
|
533
|
+
if (effect.kind === "appear") state.display = false;
|
|
534
|
+
else if (effect.kind === "fade-in") {
|
|
535
|
+
state.display = false;
|
|
536
|
+
state.opacity = 0;
|
|
537
|
+
} else if (effect.kind === "wipe") state["clip-path"] = effect.filter?.includes("right") ? "inset(0 0 0 100%)" : "inset(0 100% 0 0)";
|
|
538
|
+
else if (effect.kind === "scale") {
|
|
539
|
+
state["scale-x"] = numberValue(effect, "scaleFromX", 0);
|
|
540
|
+
state["scale-y"] = numberValue(effect, "scaleFromY", 0);
|
|
541
|
+
}
|
|
542
|
+
preparedState[target] = state;
|
|
543
|
+
}
|
|
544
|
+
const frozenInitialState = Object.freeze(Object.fromEntries(
|
|
545
|
+
Object.entries(preparedState).map(([key, value]) => [key, Object.freeze(value)])
|
|
546
|
+
));
|
|
547
|
+
const groups = schedule.groups.map((group) => {
|
|
548
|
+
const effects = group.effects.filter((effect) => compilableNodeIds.has(effect.nodeId));
|
|
549
|
+
const durationMs = effects.some((effect) => effect.endMs === "indefinite") ? "indefinite" : Math.max(0, ...effects.map((effect) => effect.endMs));
|
|
550
|
+
return Object.freeze({
|
|
551
|
+
key: group.key,
|
|
552
|
+
clickBoundary: group.clickBoundary,
|
|
553
|
+
durationMs,
|
|
554
|
+
tracks: compileFragments(effects.flatMap((effect) => fragmentsForEffect(
|
|
555
|
+
effect,
|
|
556
|
+
frozenInitialState,
|
|
557
|
+
slide.nodes[effect.nodeId]
|
|
558
|
+
))),
|
|
559
|
+
effects: Object.freeze(effects)
|
|
560
|
+
});
|
|
561
|
+
});
|
|
562
|
+
return Object.freeze({
|
|
563
|
+
slideIndex: slide.index,
|
|
564
|
+
schedule,
|
|
565
|
+
groups: Object.freeze(groups),
|
|
566
|
+
initialState: frozenInitialState
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
function segmentProgress(segment, positionMs) {
|
|
570
|
+
if (segment.endMs <= segment.startMs) return 1;
|
|
571
|
+
let progress = (positionMs - segment.startMs) / (segment.endMs - segment.startMs);
|
|
572
|
+
const easing = segment.easing?.match(/^pptx\(([^,]+),([^)]+)\)$/u);
|
|
573
|
+
if (easing) {
|
|
574
|
+
let acceleration = Math.max(0, Number(easing[1]) || 0);
|
|
575
|
+
let deceleration = Math.max(0, Number(easing[2]) || 0);
|
|
576
|
+
const total = acceleration + deceleration;
|
|
577
|
+
if (total > 1) {
|
|
578
|
+
acceleration /= total;
|
|
579
|
+
deceleration /= total;
|
|
580
|
+
}
|
|
581
|
+
const peak = 1 / (1 - (acceleration + deceleration) / 2);
|
|
582
|
+
if (acceleration > 0 && progress < acceleration) {
|
|
583
|
+
progress = peak * progress * progress / (2 * acceleration);
|
|
584
|
+
} else if (deceleration > 0 && progress > 1 - deceleration) {
|
|
585
|
+
progress = 1 - peak * (1 - progress) * (1 - progress) / (2 * deceleration);
|
|
586
|
+
} else progress = peak * (progress - acceleration / 2);
|
|
587
|
+
}
|
|
588
|
+
return Math.max(0, Math.min(1, progress));
|
|
589
|
+
}
|
|
590
|
+
function evaluateSegment(segment, positionMs) {
|
|
591
|
+
if (positionMs >= segment.endMs) return segment.to;
|
|
592
|
+
const progress = segmentProgress(segment, positionMs);
|
|
593
|
+
if (typeof segment.from === "number" && typeof segment.to === "number") {
|
|
594
|
+
return segment.from + (segment.to - segment.from) * progress;
|
|
595
|
+
}
|
|
596
|
+
if (typeof segment.from === "string" && typeof segment.to === "string") {
|
|
597
|
+
const fromInset = [...segment.from.matchAll(/-?\d+(?:\.\d+)?/gu)].map((match) => Number(match[0]));
|
|
598
|
+
const toInset = [...segment.to.matchAll(/-?\d+(?:\.\d+)?/gu)].map((match) => Number(match[0]));
|
|
599
|
+
if (segment.from.startsWith("inset(") && segment.to.startsWith("inset(") && fromInset.length === 4 && toInset.length === 4) {
|
|
600
|
+
return `inset(${fromInset.map((value, item) => `${value + (toInset[item] - value) * progress}%`).join(" ")})`;
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
return progress >= 1 ? segment.to : segment.from;
|
|
604
|
+
}
|
|
605
|
+
function evaluatePptxTrack(track, positionMs) {
|
|
606
|
+
if (track.repeatIndefinite && track.repeatPeriodMs && track.repeatStartMs !== void 0 && positionMs >= track.repeatStartMs) {
|
|
607
|
+
positionMs = track.repeatStartMs + (positionMs - track.repeatStartMs) % track.repeatPeriodMs;
|
|
608
|
+
}
|
|
609
|
+
const segments = track.segments;
|
|
610
|
+
if (segments?.length) {
|
|
611
|
+
const selected = segments.filter((segment) => {
|
|
612
|
+
if (positionMs < segment.startMs) return false;
|
|
613
|
+
if (positionMs < segment.endMs) return true;
|
|
614
|
+
return segment.fill !== "remove";
|
|
615
|
+
}).sort((left, right) => left.startMs - right.startMs || left.order - right.order).at(-1);
|
|
616
|
+
return selected ? evaluateSegment(selected, positionMs) : track.initialValue;
|
|
617
|
+
}
|
|
618
|
+
if (track.keyframes.length === 0 || positionMs < track.keyframes[0].timeMs) return track.initialValue;
|
|
619
|
+
let previous = track.keyframes[0];
|
|
620
|
+
for (let index = 1; index < track.keyframes.length; index += 1) {
|
|
621
|
+
const next = track.keyframes[index];
|
|
622
|
+
if (positionMs < next.timeMs) {
|
|
623
|
+
if (typeof previous.value === "number" && typeof next.value === "number" && next.timeMs > previous.timeMs) {
|
|
624
|
+
let progress = (positionMs - previous.timeMs) / (next.timeMs - previous.timeMs);
|
|
625
|
+
const easing = previous.easing?.match(/^pptx\(([^,]+),([^)]+)\)$/u);
|
|
626
|
+
if (easing) {
|
|
627
|
+
let acceleration = Math.max(0, Number(easing[1]) || 0);
|
|
628
|
+
let deceleration = Math.max(0, Number(easing[2]) || 0);
|
|
629
|
+
const total = acceleration + deceleration;
|
|
630
|
+
if (total > 1) {
|
|
631
|
+
acceleration /= total;
|
|
632
|
+
deceleration /= total;
|
|
633
|
+
}
|
|
634
|
+
const peak = 1 / (1 - (acceleration + deceleration) / 2);
|
|
635
|
+
if (acceleration > 0 && progress < acceleration) progress = peak * progress * progress / (2 * acceleration);
|
|
636
|
+
else if (deceleration > 0 && progress > 1 - deceleration) {
|
|
637
|
+
progress = 1 - peak * (1 - progress) * (1 - progress) / (2 * deceleration);
|
|
638
|
+
} else progress = peak * (progress - acceleration / 2);
|
|
639
|
+
}
|
|
640
|
+
return previous.value + (next.value - previous.value) * Math.max(0, Math.min(1, progress));
|
|
641
|
+
}
|
|
642
|
+
if (typeof previous.value === "string" && typeof next.value === "string") {
|
|
643
|
+
const fromInset = [...previous.value.matchAll(/-?\d+(?:\.\d+)?/gu)].map((match) => Number(match[0]));
|
|
644
|
+
const toInset = [...next.value.matchAll(/-?\d+(?:\.\d+)?/gu)].map((match) => Number(match[0]));
|
|
645
|
+
if (previous.value.startsWith("inset(") && next.value.startsWith("inset(") && fromInset.length === 4 && toInset.length === 4) {
|
|
646
|
+
const progress = Math.max(0, Math.min(1, (positionMs - previous.timeMs) / (next.timeMs - previous.timeMs)));
|
|
647
|
+
return `inset(${fromInset.map((value, item) => `${value + (toInset[item] - value) * progress}%`).join(" ")})`;
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
return previous.value;
|
|
651
|
+
}
|
|
652
|
+
previous = next;
|
|
653
|
+
}
|
|
654
|
+
return previous.value;
|
|
655
|
+
}
|
|
656
|
+
function evaluatePptxTriggerGroup(group, positionMs) {
|
|
657
|
+
const result = {};
|
|
658
|
+
for (const track of group.tracks) {
|
|
659
|
+
const target = track.paragraphRange ? `${track.objectKey}#paragraph:${track.paragraphRange.start}-${track.paragraphRange.end}` : track.objectKey;
|
|
660
|
+
const state = result[target] ?? {};
|
|
661
|
+
state[track.property] = evaluatePptxTrack(track, positionMs);
|
|
662
|
+
result[target] = state;
|
|
663
|
+
}
|
|
664
|
+
return Object.freeze(Object.fromEntries(
|
|
665
|
+
Object.entries(result).map(([key, value]) => [key, Object.freeze(value)])
|
|
666
|
+
));
|
|
667
|
+
}
|
|
668
|
+
function mergeState(target, source) {
|
|
669
|
+
for (const [objectKey, values] of Object.entries(source)) {
|
|
670
|
+
target[objectKey] = { ...target[objectKey] ?? {}, ...values };
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
function rebuildPptxStateAtBoundary(compiled, clickBoundary) {
|
|
674
|
+
const result = Object.fromEntries(
|
|
675
|
+
Object.entries(compiled.initialState).map(([key, value]) => [key, { ...value }])
|
|
676
|
+
);
|
|
677
|
+
for (const group of compiled.groups) {
|
|
678
|
+
if (group.key !== "auto" && (!group.key.startsWith("click:") || group.clickBoundary > clickBoundary)) continue;
|
|
679
|
+
const position = group.durationMs === "indefinite" ? 0 : group.durationMs;
|
|
680
|
+
mergeState(result, evaluatePptxTriggerGroup(group, position));
|
|
681
|
+
}
|
|
682
|
+
return Object.freeze(Object.fromEntries(
|
|
683
|
+
Object.entries(result).map(([key, value]) => [key, Object.freeze(value)])
|
|
684
|
+
));
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
// src/index.ts
|
|
688
|
+
var DEFAULT_PPTX_PREVIEW_LIMITS = Object.freeze({
|
|
689
|
+
maxInputBytes: 50 * 1024 * 1024,
|
|
690
|
+
maxArchiveEntries: 4e3,
|
|
691
|
+
maxSingleEntryBytes: 32 * 1024 * 1024,
|
|
692
|
+
maxUncompressedBytes: 256 * 1024 * 1024,
|
|
693
|
+
maxMediaBytes: 192 * 1024 * 1024,
|
|
694
|
+
maxConcurrency: 8
|
|
695
|
+
});
|
|
696
|
+
|
|
697
|
+
export {
|
|
698
|
+
PptxPreviewError,
|
|
699
|
+
PptxPlaybackError,
|
|
700
|
+
createPptxCapabilityReport,
|
|
701
|
+
createPptxObjectKey,
|
|
702
|
+
matchPptxMorphObjects,
|
|
703
|
+
compilePptxSlideSchedule,
|
|
704
|
+
compilePptxSlideTracks,
|
|
705
|
+
evaluatePptxTrack,
|
|
706
|
+
evaluatePptxTriggerGroup,
|
|
707
|
+
rebuildPptxStateAtBoundary,
|
|
708
|
+
DEFAULT_PPTX_PREVIEW_LIMITS
|
|
709
|
+
};
|