@particle-academy/dark-slide 0.5.2
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 +21 -0
- package/README.md +48 -0
- package/dist/index.cjs +3534 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +379 -0
- package/dist/index.d.ts +379 -0
- package/dist/index.js +3522 -0
- package/dist/index.js.map +1 -0
- package/package.json +66 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,3534 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/exceptions.ts
|
|
4
|
+
var SchemaException = class _SchemaException extends Error {
|
|
5
|
+
constructor(message, errors) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = "SchemaException";
|
|
8
|
+
this.errors = errors;
|
|
9
|
+
Object.setPrototypeOf(this, _SchemaException.prototype);
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
// src/helpers/emu.ts
|
|
14
|
+
var EMU_PER_INCH = 914400;
|
|
15
|
+
var DEFAULT_SLIDE_WIDTH = 9144e3;
|
|
16
|
+
var DEFAULT_SLIDE_HEIGHT = 5143500;
|
|
17
|
+
var Emu = {
|
|
18
|
+
EMU_PER_INCH,
|
|
19
|
+
DEFAULT_SLIDE_WIDTH,
|
|
20
|
+
DEFAULT_SLIDE_HEIGHT,
|
|
21
|
+
fromFracX(f, slideWidthEmu = DEFAULT_SLIDE_WIDTH) {
|
|
22
|
+
return Math.round(f * slideWidthEmu);
|
|
23
|
+
},
|
|
24
|
+
fromFracY(f, slideHeightEmu = DEFAULT_SLIDE_HEIGHT) {
|
|
25
|
+
return Math.round(f * slideHeightEmu);
|
|
26
|
+
},
|
|
27
|
+
toFracX(emu, slideWidthEmu = DEFAULT_SLIDE_WIDTH) {
|
|
28
|
+
return slideWidthEmu === 0 ? 0 : emu / slideWidthEmu;
|
|
29
|
+
},
|
|
30
|
+
toFracY(emu, slideHeightEmu = DEFAULT_SLIDE_HEIGHT) {
|
|
31
|
+
return slideHeightEmu === 0 ? 0 : emu / slideHeightEmu;
|
|
32
|
+
},
|
|
33
|
+
fromPt(pt) {
|
|
34
|
+
return Math.round(pt * (EMU_PER_INCH / 72));
|
|
35
|
+
},
|
|
36
|
+
hundredthsOfPoint(pt) {
|
|
37
|
+
return Math.round(pt * 100);
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// src/reader/xml.ts
|
|
42
|
+
function localName(name) {
|
|
43
|
+
const idx = name.indexOf(":");
|
|
44
|
+
return idx >= 0 ? name.slice(idx + 1) : name;
|
|
45
|
+
}
|
|
46
|
+
function unescapeXml(s) {
|
|
47
|
+
if (s.indexOf("&") < 0) return s;
|
|
48
|
+
return s.replace(/&(#x?[0-9a-fA-F]+|\w+);/g, (m, ent) => {
|
|
49
|
+
switch (ent) {
|
|
50
|
+
case "amp":
|
|
51
|
+
return "&";
|
|
52
|
+
case "lt":
|
|
53
|
+
return "<";
|
|
54
|
+
case "gt":
|
|
55
|
+
return ">";
|
|
56
|
+
case "quot":
|
|
57
|
+
return '"';
|
|
58
|
+
case "apos":
|
|
59
|
+
return "'";
|
|
60
|
+
default:
|
|
61
|
+
if (ent[0] === "#") {
|
|
62
|
+
const code = ent[1] === "x" || ent[1] === "X" ? parseInt(ent.slice(2), 16) : parseInt(ent.slice(1), 10);
|
|
63
|
+
return Number.isFinite(code) ? String.fromCodePoint(code) : m;
|
|
64
|
+
}
|
|
65
|
+
return m;
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
function findTagEnd(src, from) {
|
|
70
|
+
let quote = "";
|
|
71
|
+
for (let i = from; i < src.length; i++) {
|
|
72
|
+
const ch = src[i];
|
|
73
|
+
if (quote) {
|
|
74
|
+
if (ch === quote) quote = "";
|
|
75
|
+
} else if (ch === '"' || ch === "'") {
|
|
76
|
+
quote = ch;
|
|
77
|
+
} else if (ch === ">") {
|
|
78
|
+
return i;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return src.length;
|
|
82
|
+
}
|
|
83
|
+
var ATTR_RE = /([^\s=/]+)\s*=\s*("([^"]*)"|'([^']*)')/g;
|
|
84
|
+
function parseTag(inner) {
|
|
85
|
+
const trimmed = inner.trim();
|
|
86
|
+
let i = 0;
|
|
87
|
+
while (i < trimmed.length && !/\s/.test(trimmed[i])) i++;
|
|
88
|
+
const name = trimmed.slice(0, i);
|
|
89
|
+
const attrs = {};
|
|
90
|
+
const rest = trimmed.slice(i);
|
|
91
|
+
for (const m of rest.matchAll(ATTR_RE)) {
|
|
92
|
+
const key = localName(m[1]);
|
|
93
|
+
const value = unescapeXml(m[3] ?? m[4] ?? "");
|
|
94
|
+
attrs[key] = value;
|
|
95
|
+
}
|
|
96
|
+
return { name, attrs };
|
|
97
|
+
}
|
|
98
|
+
function parseXml(src) {
|
|
99
|
+
let i = 0;
|
|
100
|
+
const n = src.length;
|
|
101
|
+
const root = { name: "#root", attrs: {}, children: [], text: "" };
|
|
102
|
+
const stack = [root];
|
|
103
|
+
while (i < n) {
|
|
104
|
+
if (src[i] === "<") {
|
|
105
|
+
if (src.startsWith("<!--", i)) {
|
|
106
|
+
const end2 = src.indexOf("-->", i);
|
|
107
|
+
i = end2 < 0 ? n : end2 + 3;
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
if (src.startsWith("<![CDATA[", i)) {
|
|
111
|
+
const end2 = src.indexOf("]]>", i);
|
|
112
|
+
stack[stack.length - 1].text += src.slice(i + 9, end2 < 0 ? n : end2);
|
|
113
|
+
i = end2 < 0 ? n : end2 + 3;
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
if (src.startsWith("<?", i)) {
|
|
117
|
+
const end2 = src.indexOf("?>", i);
|
|
118
|
+
i = end2 < 0 ? n : end2 + 2;
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
if (src.startsWith("<!", i)) {
|
|
122
|
+
const end2 = src.indexOf(">", i);
|
|
123
|
+
i = end2 < 0 ? n : end2 + 1;
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
if (src[i + 1] === "/") {
|
|
127
|
+
const end2 = src.indexOf(">", i);
|
|
128
|
+
if (stack.length > 1) stack.pop();
|
|
129
|
+
i = end2 < 0 ? n : end2 + 1;
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
const end = findTagEnd(src, i + 1);
|
|
133
|
+
const tagContent = src.slice(i + 1, end);
|
|
134
|
+
const selfClosing = tagContent.endsWith("/");
|
|
135
|
+
const { name, attrs } = parseTag(selfClosing ? tagContent.slice(0, -1) : tagContent);
|
|
136
|
+
const node = { name: localName(name), attrs, children: [], text: "" };
|
|
137
|
+
stack[stack.length - 1].children.push(node);
|
|
138
|
+
if (!selfClosing) stack.push(node);
|
|
139
|
+
i = end + 1;
|
|
140
|
+
} else {
|
|
141
|
+
const next = src.indexOf("<", i);
|
|
142
|
+
const stop = next < 0 ? n : next;
|
|
143
|
+
stack[stack.length - 1].text += unescapeXml(src.slice(i, stop));
|
|
144
|
+
i = stop;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return root.children[0] ?? null;
|
|
148
|
+
}
|
|
149
|
+
function el(node, name) {
|
|
150
|
+
return node?.children.find((c) => c.name === name);
|
|
151
|
+
}
|
|
152
|
+
function at(node, name) {
|
|
153
|
+
return node?.attrs[name];
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// src/zip/crc32.ts
|
|
157
|
+
var TABLE = /* @__PURE__ */ (() => {
|
|
158
|
+
const t = new Uint32Array(256);
|
|
159
|
+
for (let n = 0; n < 256; n++) {
|
|
160
|
+
let c = n;
|
|
161
|
+
for (let k = 0; k < 8; k++) {
|
|
162
|
+
c = c & 1 ? 3988292384 ^ c >>> 1 : c >>> 1;
|
|
163
|
+
}
|
|
164
|
+
t[n] = c >>> 0;
|
|
165
|
+
}
|
|
166
|
+
return t;
|
|
167
|
+
})();
|
|
168
|
+
function crc32(bytes) {
|
|
169
|
+
let crc = 4294967295;
|
|
170
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
171
|
+
crc = TABLE[(crc ^ bytes[i]) & 255] ^ crc >>> 8;
|
|
172
|
+
}
|
|
173
|
+
return (crc ^ 4294967295) >>> 0;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// src/zip/zip-writer.ts
|
|
177
|
+
var encoder = new TextEncoder();
|
|
178
|
+
var DOS_TIME = 0;
|
|
179
|
+
var DOS_DATE = 33;
|
|
180
|
+
function zipSync(files) {
|
|
181
|
+
const localParts = [];
|
|
182
|
+
const centralParts = [];
|
|
183
|
+
let offset = 0;
|
|
184
|
+
for (const f of files) {
|
|
185
|
+
const nameBytes = encoder.encode(f.name);
|
|
186
|
+
const crc = crc32(f.data);
|
|
187
|
+
const size = f.data.length;
|
|
188
|
+
const lh = new Uint8Array(30 + nameBytes.length);
|
|
189
|
+
const ldv = new DataView(lh.buffer);
|
|
190
|
+
ldv.setUint32(0, 67324752, true);
|
|
191
|
+
ldv.setUint16(4, 20, true);
|
|
192
|
+
ldv.setUint16(6, 2048, true);
|
|
193
|
+
ldv.setUint16(8, 0, true);
|
|
194
|
+
ldv.setUint16(10, DOS_TIME, true);
|
|
195
|
+
ldv.setUint16(12, DOS_DATE, true);
|
|
196
|
+
ldv.setUint32(14, crc, true);
|
|
197
|
+
ldv.setUint32(18, size, true);
|
|
198
|
+
ldv.setUint32(22, size, true);
|
|
199
|
+
ldv.setUint16(26, nameBytes.length, true);
|
|
200
|
+
ldv.setUint16(28, 0, true);
|
|
201
|
+
lh.set(nameBytes, 30);
|
|
202
|
+
localParts.push(lh, f.data);
|
|
203
|
+
const cd = new Uint8Array(46 + nameBytes.length);
|
|
204
|
+
const cdv = new DataView(cd.buffer);
|
|
205
|
+
cdv.setUint32(0, 33639248, true);
|
|
206
|
+
cdv.setUint16(4, 20, true);
|
|
207
|
+
cdv.setUint16(6, 20, true);
|
|
208
|
+
cdv.setUint16(8, 2048, true);
|
|
209
|
+
cdv.setUint16(10, 0, true);
|
|
210
|
+
cdv.setUint16(12, DOS_TIME, true);
|
|
211
|
+
cdv.setUint16(14, DOS_DATE, true);
|
|
212
|
+
cdv.setUint32(16, crc, true);
|
|
213
|
+
cdv.setUint32(20, size, true);
|
|
214
|
+
cdv.setUint32(24, size, true);
|
|
215
|
+
cdv.setUint16(28, nameBytes.length, true);
|
|
216
|
+
cdv.setUint16(30, 0, true);
|
|
217
|
+
cdv.setUint16(32, 0, true);
|
|
218
|
+
cdv.setUint16(34, 0, true);
|
|
219
|
+
cdv.setUint16(36, 0, true);
|
|
220
|
+
cdv.setUint32(38, 0, true);
|
|
221
|
+
cdv.setUint32(42, offset, true);
|
|
222
|
+
cd.set(nameBytes, 46);
|
|
223
|
+
centralParts.push(cd);
|
|
224
|
+
offset += lh.length + f.data.length;
|
|
225
|
+
}
|
|
226
|
+
const localSize = offset;
|
|
227
|
+
const centralSize = centralParts.reduce((s, c) => s + c.length, 0);
|
|
228
|
+
const eocd = new Uint8Array(22);
|
|
229
|
+
const edv = new DataView(eocd.buffer);
|
|
230
|
+
edv.setUint32(0, 101010256, true);
|
|
231
|
+
edv.setUint16(4, 0, true);
|
|
232
|
+
edv.setUint16(6, 0, true);
|
|
233
|
+
edv.setUint16(8, files.length, true);
|
|
234
|
+
edv.setUint16(10, files.length, true);
|
|
235
|
+
edv.setUint32(12, centralSize, true);
|
|
236
|
+
edv.setUint32(16, localSize, true);
|
|
237
|
+
edv.setUint16(20, 0, true);
|
|
238
|
+
const out = new Uint8Array(localSize + centralSize + 22);
|
|
239
|
+
let p = 0;
|
|
240
|
+
for (const part of localParts) {
|
|
241
|
+
out.set(part, p);
|
|
242
|
+
p += part.length;
|
|
243
|
+
}
|
|
244
|
+
for (const part of centralParts) {
|
|
245
|
+
out.set(part, p);
|
|
246
|
+
p += part.length;
|
|
247
|
+
}
|
|
248
|
+
out.set(eocd, p);
|
|
249
|
+
return out;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// src/zip/inflate.ts
|
|
253
|
+
var Tree = class {
|
|
254
|
+
constructor() {
|
|
255
|
+
// number of codes with a given bit length
|
|
256
|
+
this.table = new Uint16Array(16);
|
|
257
|
+
// symbols sorted by code
|
|
258
|
+
this.trans = new Uint16Array(288);
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
var LENGTH_BITS = new Uint8Array([
|
|
262
|
+
0,
|
|
263
|
+
0,
|
|
264
|
+
0,
|
|
265
|
+
0,
|
|
266
|
+
0,
|
|
267
|
+
0,
|
|
268
|
+
0,
|
|
269
|
+
0,
|
|
270
|
+
1,
|
|
271
|
+
1,
|
|
272
|
+
1,
|
|
273
|
+
1,
|
|
274
|
+
2,
|
|
275
|
+
2,
|
|
276
|
+
2,
|
|
277
|
+
2,
|
|
278
|
+
3,
|
|
279
|
+
3,
|
|
280
|
+
3,
|
|
281
|
+
3,
|
|
282
|
+
4,
|
|
283
|
+
4,
|
|
284
|
+
4,
|
|
285
|
+
4,
|
|
286
|
+
5,
|
|
287
|
+
5,
|
|
288
|
+
5,
|
|
289
|
+
5,
|
|
290
|
+
0
|
|
291
|
+
]);
|
|
292
|
+
var LENGTH_BASE = new Uint16Array([
|
|
293
|
+
3,
|
|
294
|
+
4,
|
|
295
|
+
5,
|
|
296
|
+
6,
|
|
297
|
+
7,
|
|
298
|
+
8,
|
|
299
|
+
9,
|
|
300
|
+
10,
|
|
301
|
+
11,
|
|
302
|
+
13,
|
|
303
|
+
15,
|
|
304
|
+
17,
|
|
305
|
+
19,
|
|
306
|
+
23,
|
|
307
|
+
27,
|
|
308
|
+
31,
|
|
309
|
+
35,
|
|
310
|
+
43,
|
|
311
|
+
51,
|
|
312
|
+
59,
|
|
313
|
+
67,
|
|
314
|
+
83,
|
|
315
|
+
99,
|
|
316
|
+
115,
|
|
317
|
+
131,
|
|
318
|
+
163,
|
|
319
|
+
195,
|
|
320
|
+
227,
|
|
321
|
+
258
|
|
322
|
+
]);
|
|
323
|
+
var DIST_BITS = new Uint8Array([
|
|
324
|
+
0,
|
|
325
|
+
0,
|
|
326
|
+
0,
|
|
327
|
+
0,
|
|
328
|
+
1,
|
|
329
|
+
1,
|
|
330
|
+
2,
|
|
331
|
+
2,
|
|
332
|
+
3,
|
|
333
|
+
3,
|
|
334
|
+
4,
|
|
335
|
+
4,
|
|
336
|
+
5,
|
|
337
|
+
5,
|
|
338
|
+
6,
|
|
339
|
+
6,
|
|
340
|
+
7,
|
|
341
|
+
7,
|
|
342
|
+
8,
|
|
343
|
+
8,
|
|
344
|
+
9,
|
|
345
|
+
9,
|
|
346
|
+
10,
|
|
347
|
+
10,
|
|
348
|
+
11,
|
|
349
|
+
11,
|
|
350
|
+
12,
|
|
351
|
+
12,
|
|
352
|
+
13,
|
|
353
|
+
13
|
|
354
|
+
]);
|
|
355
|
+
var DIST_BASE = new Uint16Array([
|
|
356
|
+
1,
|
|
357
|
+
2,
|
|
358
|
+
3,
|
|
359
|
+
4,
|
|
360
|
+
5,
|
|
361
|
+
7,
|
|
362
|
+
9,
|
|
363
|
+
13,
|
|
364
|
+
17,
|
|
365
|
+
25,
|
|
366
|
+
33,
|
|
367
|
+
49,
|
|
368
|
+
65,
|
|
369
|
+
97,
|
|
370
|
+
129,
|
|
371
|
+
193,
|
|
372
|
+
257,
|
|
373
|
+
385,
|
|
374
|
+
513,
|
|
375
|
+
769,
|
|
376
|
+
1025,
|
|
377
|
+
1537,
|
|
378
|
+
2049,
|
|
379
|
+
3073,
|
|
380
|
+
4097,
|
|
381
|
+
6145,
|
|
382
|
+
8193,
|
|
383
|
+
12289,
|
|
384
|
+
16385,
|
|
385
|
+
24577
|
|
386
|
+
]);
|
|
387
|
+
var CLC_INDEX = new Uint8Array([
|
|
388
|
+
16,
|
|
389
|
+
17,
|
|
390
|
+
18,
|
|
391
|
+
0,
|
|
392
|
+
8,
|
|
393
|
+
7,
|
|
394
|
+
9,
|
|
395
|
+
6,
|
|
396
|
+
10,
|
|
397
|
+
5,
|
|
398
|
+
11,
|
|
399
|
+
4,
|
|
400
|
+
12,
|
|
401
|
+
3,
|
|
402
|
+
13,
|
|
403
|
+
2,
|
|
404
|
+
14,
|
|
405
|
+
1,
|
|
406
|
+
15
|
|
407
|
+
]);
|
|
408
|
+
function buildFixedTrees(lt, dt) {
|
|
409
|
+
let i;
|
|
410
|
+
for (i = 0; i < 7; i++) lt.table[i] = 0;
|
|
411
|
+
lt.table[7] = 24;
|
|
412
|
+
lt.table[8] = 152;
|
|
413
|
+
lt.table[9] = 112;
|
|
414
|
+
for (i = 0; i < 24; i++) lt.trans[i] = 256 + i;
|
|
415
|
+
for (i = 0; i < 144; i++) lt.trans[24 + i] = i;
|
|
416
|
+
for (i = 0; i < 8; i++) lt.trans[24 + 144 + i] = 280 + i;
|
|
417
|
+
for (i = 0; i < 112; i++) lt.trans[24 + 144 + 8 + i] = 144 + i;
|
|
418
|
+
for (i = 0; i < 5; i++) dt.table[i] = 0;
|
|
419
|
+
dt.table[5] = 32;
|
|
420
|
+
for (i = 0; i < 32; i++) dt.trans[i] = i;
|
|
421
|
+
}
|
|
422
|
+
var OFFS = new Uint16Array(16);
|
|
423
|
+
function buildTree(t, lengths, off, num) {
|
|
424
|
+
let i;
|
|
425
|
+
let sum = 0;
|
|
426
|
+
for (i = 0; i < 16; i++) t.table[i] = 0;
|
|
427
|
+
for (i = 0; i < num; i++) t.table[lengths[off + i]]++;
|
|
428
|
+
t.table[0] = 0;
|
|
429
|
+
for (i = 0; i < 16; i++) {
|
|
430
|
+
OFFS[i] = sum;
|
|
431
|
+
sum += t.table[i];
|
|
432
|
+
}
|
|
433
|
+
for (i = 0; i < num; i++) {
|
|
434
|
+
const len = lengths[off + i];
|
|
435
|
+
if (len) t.trans[OFFS[len]++] = i;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
var Reader = class {
|
|
439
|
+
constructor(source) {
|
|
440
|
+
this.index = 0;
|
|
441
|
+
this.tag = 0;
|
|
442
|
+
this.bitcount = 0;
|
|
443
|
+
this.source = source;
|
|
444
|
+
}
|
|
445
|
+
};
|
|
446
|
+
function getBit(d) {
|
|
447
|
+
if (d.bitcount-- === 0) {
|
|
448
|
+
d.tag = d.source[d.index++];
|
|
449
|
+
d.bitcount = 7;
|
|
450
|
+
}
|
|
451
|
+
const bit = d.tag & 1;
|
|
452
|
+
d.tag >>>= 1;
|
|
453
|
+
return bit;
|
|
454
|
+
}
|
|
455
|
+
function readBits(d, num, base) {
|
|
456
|
+
if (!num) return base;
|
|
457
|
+
let val = 0;
|
|
458
|
+
for (let i = 0; i < num; i++) val |= getBit(d) << i;
|
|
459
|
+
return val + base;
|
|
460
|
+
}
|
|
461
|
+
function decodeSymbol(d, t) {
|
|
462
|
+
let sum = 0;
|
|
463
|
+
let cur = 0;
|
|
464
|
+
let len = 0;
|
|
465
|
+
do {
|
|
466
|
+
cur = 2 * cur + getBit(d);
|
|
467
|
+
len++;
|
|
468
|
+
sum += t.table[len];
|
|
469
|
+
cur -= t.table[len];
|
|
470
|
+
} while (cur >= 0);
|
|
471
|
+
return t.trans[sum + cur];
|
|
472
|
+
}
|
|
473
|
+
function decodeTrees(d, lt, dt) {
|
|
474
|
+
const lengths = new Uint8Array(288 + 32);
|
|
475
|
+
const hlit = readBits(d, 5, 257);
|
|
476
|
+
const hdist = readBits(d, 5, 1);
|
|
477
|
+
const hclen = readBits(d, 4, 4);
|
|
478
|
+
let i;
|
|
479
|
+
for (i = 0; i < 19; i++) lengths[i] = 0;
|
|
480
|
+
for (i = 0; i < hclen; i++) {
|
|
481
|
+
const clen = readBits(d, 3, 0);
|
|
482
|
+
lengths[CLC_INDEX[i]] = clen;
|
|
483
|
+
}
|
|
484
|
+
const codeTree = new Tree();
|
|
485
|
+
buildTree(codeTree, lengths, 0, 19);
|
|
486
|
+
for (let num = 0; num < hlit + hdist; ) {
|
|
487
|
+
const sym = decodeSymbol(d, codeTree);
|
|
488
|
+
switch (sym) {
|
|
489
|
+
case 16: {
|
|
490
|
+
const prev = lengths[num - 1];
|
|
491
|
+
for (let length = readBits(d, 2, 3); length; length--) lengths[num++] = prev;
|
|
492
|
+
break;
|
|
493
|
+
}
|
|
494
|
+
case 17:
|
|
495
|
+
for (let length = readBits(d, 3, 3); length; length--) lengths[num++] = 0;
|
|
496
|
+
break;
|
|
497
|
+
case 18:
|
|
498
|
+
for (let length = readBits(d, 7, 11); length; length--) lengths[num++] = 0;
|
|
499
|
+
break;
|
|
500
|
+
default:
|
|
501
|
+
lengths[num++] = sym;
|
|
502
|
+
break;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
buildTree(lt, lengths, 0, hlit);
|
|
506
|
+
buildTree(dt, lengths, hlit, hdist);
|
|
507
|
+
}
|
|
508
|
+
var Out = class {
|
|
509
|
+
constructor() {
|
|
510
|
+
this.buf = new Uint8Array(1024);
|
|
511
|
+
this.len = 0;
|
|
512
|
+
}
|
|
513
|
+
push(b) {
|
|
514
|
+
if (this.len >= this.buf.length) {
|
|
515
|
+
const next = new Uint8Array(this.buf.length * 2);
|
|
516
|
+
next.set(this.buf);
|
|
517
|
+
this.buf = next;
|
|
518
|
+
}
|
|
519
|
+
this.buf[this.len++] = b;
|
|
520
|
+
}
|
|
521
|
+
};
|
|
522
|
+
function inflateBlockData(d, out, lt, dt) {
|
|
523
|
+
for (; ; ) {
|
|
524
|
+
const sym = decodeSymbol(d, lt);
|
|
525
|
+
if (sym === 256) return;
|
|
526
|
+
if (sym < 256) {
|
|
527
|
+
out.push(sym);
|
|
528
|
+
} else {
|
|
529
|
+
const lengthSym = sym - 257;
|
|
530
|
+
const length = readBits(d, LENGTH_BITS[lengthSym], LENGTH_BASE[lengthSym]);
|
|
531
|
+
const distSym = decodeSymbol(d, dt);
|
|
532
|
+
const dist = readBits(d, DIST_BITS[distSym], DIST_BASE[distSym]);
|
|
533
|
+
const offs = out.len - dist;
|
|
534
|
+
for (let i = 0; i < length; i++) out.push(out.buf[offs + i]);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
function inflateUncompressedBlock(d, out) {
|
|
539
|
+
d.bitcount = 0;
|
|
540
|
+
let length = d.source[d.index + 1] * 256 + d.source[d.index];
|
|
541
|
+
d.index += 4;
|
|
542
|
+
for (; length; length--) out.push(d.source[d.index++]);
|
|
543
|
+
}
|
|
544
|
+
function inflateRaw(source) {
|
|
545
|
+
const d = new Reader(source);
|
|
546
|
+
const out = new Out();
|
|
547
|
+
const lt = new Tree();
|
|
548
|
+
const dt = new Tree();
|
|
549
|
+
let bfinal;
|
|
550
|
+
do {
|
|
551
|
+
bfinal = getBit(d);
|
|
552
|
+
const btype = readBits(d, 2, 0);
|
|
553
|
+
if (btype === 0) {
|
|
554
|
+
inflateUncompressedBlock(d, out);
|
|
555
|
+
} else if (btype === 1) {
|
|
556
|
+
buildFixedTrees(lt, dt);
|
|
557
|
+
inflateBlockData(d, out, lt, dt);
|
|
558
|
+
} else if (btype === 2) {
|
|
559
|
+
decodeTrees(d, lt, dt);
|
|
560
|
+
inflateBlockData(d, out, lt, dt);
|
|
561
|
+
} else {
|
|
562
|
+
throw new Error("dark-slide: invalid DEFLATE block type");
|
|
563
|
+
}
|
|
564
|
+
} while (!bfinal);
|
|
565
|
+
return out.buf.subarray(0, out.len);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// src/zip/zip-reader.ts
|
|
569
|
+
var decoder = new TextDecoder();
|
|
570
|
+
function unzipSync(data) {
|
|
571
|
+
const dv = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
572
|
+
let eocd = -1;
|
|
573
|
+
for (let i = data.length - 22; i >= 0; i--) {
|
|
574
|
+
if (dv.getUint32(i, true) === 101010256) {
|
|
575
|
+
eocd = i;
|
|
576
|
+
break;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
if (eocd < 0) throw new Error("dark-slide: not a zip archive (no EOCD record)");
|
|
580
|
+
const count = dv.getUint16(eocd + 10, true);
|
|
581
|
+
let cd = dv.getUint32(eocd + 16, true);
|
|
582
|
+
const result = {};
|
|
583
|
+
for (let n = 0; n < count; n++) {
|
|
584
|
+
if (dv.getUint32(cd, true) !== 33639248) break;
|
|
585
|
+
const method = dv.getUint16(cd + 10, true);
|
|
586
|
+
const compSize = dv.getUint32(cd + 20, true);
|
|
587
|
+
const nameLen = dv.getUint16(cd + 28, true);
|
|
588
|
+
const extraLen = dv.getUint16(cd + 30, true);
|
|
589
|
+
const commentLen = dv.getUint16(cd + 32, true);
|
|
590
|
+
const localOffset = dv.getUint32(cd + 42, true);
|
|
591
|
+
const name = decoder.decode(data.subarray(cd + 46, cd + 46 + nameLen));
|
|
592
|
+
const lhNameLen = dv.getUint16(localOffset + 26, true);
|
|
593
|
+
const lhExtraLen = dv.getUint16(localOffset + 28, true);
|
|
594
|
+
const dataStart = localOffset + 30 + lhNameLen + lhExtraLen;
|
|
595
|
+
const raw = data.subarray(dataStart, dataStart + compSize);
|
|
596
|
+
let content;
|
|
597
|
+
if (method === 0) content = raw.slice();
|
|
598
|
+
else if (method === 8) content = inflateRaw(raw);
|
|
599
|
+
else throw new Error(`dark-slide: unsupported zip method ${method} for "${name}"`);
|
|
600
|
+
result[name] = content;
|
|
601
|
+
cd += 46 + nameLen + extraLen + commentLen;
|
|
602
|
+
}
|
|
603
|
+
return result;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// src/reader/pptx-reader.ts
|
|
607
|
+
var DECODER = new TextDecoder();
|
|
608
|
+
function base64Encode(bytes) {
|
|
609
|
+
if (typeof Buffer !== "undefined") {
|
|
610
|
+
return Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength).toString("base64");
|
|
611
|
+
}
|
|
612
|
+
let bin = "";
|
|
613
|
+
for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);
|
|
614
|
+
return btoa(bin);
|
|
615
|
+
}
|
|
616
|
+
function basename(path) {
|
|
617
|
+
const parts = path.split("/");
|
|
618
|
+
return parts[parts.length - 1];
|
|
619
|
+
}
|
|
620
|
+
function dirname(path) {
|
|
621
|
+
const idx = path.lastIndexOf("/");
|
|
622
|
+
return idx < 0 ? "." : path.slice(0, idx);
|
|
623
|
+
}
|
|
624
|
+
function extension(path) {
|
|
625
|
+
const base = basename(path);
|
|
626
|
+
const idx = base.lastIndexOf(".");
|
|
627
|
+
return idx < 0 ? "" : base.slice(idx + 1).toLowerCase();
|
|
628
|
+
}
|
|
629
|
+
function descendant(node, name) {
|
|
630
|
+
if (!node) return void 0;
|
|
631
|
+
for (const child of node.children) {
|
|
632
|
+
if (child.name === name) return child;
|
|
633
|
+
const found = descendant(child, name);
|
|
634
|
+
if (found) return found;
|
|
635
|
+
}
|
|
636
|
+
return void 0;
|
|
637
|
+
}
|
|
638
|
+
function descendants(node, name) {
|
|
639
|
+
const out = [];
|
|
640
|
+
if (!node) return out;
|
|
641
|
+
for (const child of node.children) {
|
|
642
|
+
if (child.name === name) out.push(child);
|
|
643
|
+
out.push(...descendants(child, name));
|
|
644
|
+
}
|
|
645
|
+
return out;
|
|
646
|
+
}
|
|
647
|
+
function deepText(node) {
|
|
648
|
+
let s = node.text;
|
|
649
|
+
for (const child of node.children) s += deepText(child);
|
|
650
|
+
return s;
|
|
651
|
+
}
|
|
652
|
+
var PptxReader = class {
|
|
653
|
+
constructor() {
|
|
654
|
+
this.currentSlideRels = {};
|
|
655
|
+
this.parts = {};
|
|
656
|
+
}
|
|
657
|
+
/** Read a PPTX file's bytes into a Deck schema object. */
|
|
658
|
+
read(bytes) {
|
|
659
|
+
return this.fromBytes(bytes);
|
|
660
|
+
}
|
|
661
|
+
fromBytes(bytes) {
|
|
662
|
+
this.parts = unzipSync(bytes);
|
|
663
|
+
return this.extract();
|
|
664
|
+
}
|
|
665
|
+
getPart(name) {
|
|
666
|
+
const p = this.parts[name];
|
|
667
|
+
return p === void 0 ? false : DECODER.decode(p);
|
|
668
|
+
}
|
|
669
|
+
extract() {
|
|
670
|
+
const deck = {
|
|
671
|
+
id: "imported-" + (Math.floor(Date.now() / 1e3) & 16777215).toString(16),
|
|
672
|
+
title: this.readCoreTitle() ?? "Imported",
|
|
673
|
+
theme: { name: "imported" },
|
|
674
|
+
slides: []
|
|
675
|
+
};
|
|
676
|
+
const presentationRels = this.getPart("ppt/_rels/presentation.xml.rels");
|
|
677
|
+
if (presentationRels === false) {
|
|
678
|
+
return deck;
|
|
679
|
+
}
|
|
680
|
+
const slideTargets = this.extractSlideTargets(presentationRels);
|
|
681
|
+
slideTargets.forEach((slideTarget, i) => {
|
|
682
|
+
const slideXml = this.getPart("ppt/" + slideTarget);
|
|
683
|
+
if (slideXml === false) {
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
686
|
+
const slideRels = this.getPart("ppt/" + dirname(slideTarget) + "/_rels/" + basename(slideTarget) + ".rels") || "";
|
|
687
|
+
const notes = this.readNotesFor(slideRels);
|
|
688
|
+
this.currentSlideRels = this.parseSlideRels(slideRels, slideTarget);
|
|
689
|
+
const slide = this.parseSlide(slideXml, "imported-slide-" + (i + 1), notes);
|
|
690
|
+
deck.slides.push(slide);
|
|
691
|
+
});
|
|
692
|
+
return deck;
|
|
693
|
+
}
|
|
694
|
+
parseSlideRels(relsXml, slideTargetRelative) {
|
|
695
|
+
if (relsXml === "") {
|
|
696
|
+
return {};
|
|
697
|
+
}
|
|
698
|
+
const root = parseXml(relsXml);
|
|
699
|
+
if (!root) {
|
|
700
|
+
return {};
|
|
701
|
+
}
|
|
702
|
+
const slideDirAbs = "ppt/" + dirname(slideTargetRelative);
|
|
703
|
+
const rels = {};
|
|
704
|
+
for (const r of descendants(root, "Relationship")) {
|
|
705
|
+
const id = at(r, "Id") ?? "";
|
|
706
|
+
const type = at(r, "Type") ?? "";
|
|
707
|
+
const target = at(r, "Target") ?? "";
|
|
708
|
+
const resolved = this.resolveRelTarget(slideDirAbs, target);
|
|
709
|
+
rels[id] = { type, target: resolved };
|
|
710
|
+
}
|
|
711
|
+
return rels;
|
|
712
|
+
}
|
|
713
|
+
resolveRelTarget(baseDir, target) {
|
|
714
|
+
if (target.startsWith("/")) {
|
|
715
|
+
return target.replace(/^\/+/, "");
|
|
716
|
+
}
|
|
717
|
+
const stack = baseDir.split("/");
|
|
718
|
+
for (const segment of target.split("/")) {
|
|
719
|
+
if (segment === "..") {
|
|
720
|
+
stack.pop();
|
|
721
|
+
} else if (segment !== "." && segment !== "") {
|
|
722
|
+
stack.push(segment);
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
return stack.join("/");
|
|
726
|
+
}
|
|
727
|
+
extractSlideTargets(relsXml) {
|
|
728
|
+
const targets = [];
|
|
729
|
+
const root = parseXml(relsXml);
|
|
730
|
+
if (!root) {
|
|
731
|
+
return [];
|
|
732
|
+
}
|
|
733
|
+
for (const r of descendants(root, "Relationship")) {
|
|
734
|
+
const type = at(r, "Type") ?? "";
|
|
735
|
+
if (type.endsWith("/slide")) {
|
|
736
|
+
targets.push(at(r, "Target") ?? "");
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
return targets;
|
|
740
|
+
}
|
|
741
|
+
readCoreTitle() {
|
|
742
|
+
const xml = this.getPart("docProps/core.xml");
|
|
743
|
+
if (xml === false) {
|
|
744
|
+
return null;
|
|
745
|
+
}
|
|
746
|
+
const root = parseXml(xml);
|
|
747
|
+
if (!root) {
|
|
748
|
+
return null;
|
|
749
|
+
}
|
|
750
|
+
const title = descendant(root, "title");
|
|
751
|
+
if (title) {
|
|
752
|
+
return deepText(title);
|
|
753
|
+
}
|
|
754
|
+
return null;
|
|
755
|
+
}
|
|
756
|
+
readNotesFor(slideRelsXml) {
|
|
757
|
+
if (slideRelsXml === "") {
|
|
758
|
+
return null;
|
|
759
|
+
}
|
|
760
|
+
const root = parseXml(slideRelsXml);
|
|
761
|
+
if (!root) {
|
|
762
|
+
return null;
|
|
763
|
+
}
|
|
764
|
+
for (const r of descendants(root, "Relationship")) {
|
|
765
|
+
if ((at(r, "Type") ?? "").endsWith("/notesSlide")) {
|
|
766
|
+
const target = at(r, "Target") ?? "";
|
|
767
|
+
const notesXml = this.getPart("ppt/" + target.replace(/\.\.\//g, "").replace(/^\/+/, ""));
|
|
768
|
+
if (notesXml === false) {
|
|
769
|
+
return null;
|
|
770
|
+
}
|
|
771
|
+
return this.parseNotesText(notesXml);
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
return null;
|
|
775
|
+
}
|
|
776
|
+
parseNotesText(xml) {
|
|
777
|
+
const root = parseXml(xml);
|
|
778
|
+
if (!root) {
|
|
779
|
+
return "";
|
|
780
|
+
}
|
|
781
|
+
const parts = [];
|
|
782
|
+
for (const t of descendants(root, "t")) {
|
|
783
|
+
parts.push(deepText(t));
|
|
784
|
+
}
|
|
785
|
+
return parts.join("\n");
|
|
786
|
+
}
|
|
787
|
+
parseSlide(xml, id, notes) {
|
|
788
|
+
const slide = {
|
|
789
|
+
id,
|
|
790
|
+
layout: "blank",
|
|
791
|
+
elements: []
|
|
792
|
+
};
|
|
793
|
+
if (notes !== null && notes !== "") {
|
|
794
|
+
slide.notes = notes;
|
|
795
|
+
}
|
|
796
|
+
const root = parseXml(xml);
|
|
797
|
+
if (!root) {
|
|
798
|
+
return slide;
|
|
799
|
+
}
|
|
800
|
+
const bg = this.parseBackground(root);
|
|
801
|
+
if (bg !== null) {
|
|
802
|
+
slide.background = bg;
|
|
803
|
+
}
|
|
804
|
+
for (const shape of descendants(root, "sp")) {
|
|
805
|
+
const element = this.parseShape(shape);
|
|
806
|
+
if (element !== null) {
|
|
807
|
+
slide.elements.push(element);
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
for (const pic of descendants(root, "pic")) {
|
|
811
|
+
const element = this.parsePic(pic);
|
|
812
|
+
if (element !== null) {
|
|
813
|
+
slide.elements.push(element);
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
for (const gf of descendants(root, "graphicFrame")) {
|
|
817
|
+
const element = this.parseGraphicFrame(gf);
|
|
818
|
+
if (element !== null) {
|
|
819
|
+
slide.elements.push(element);
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
return slide;
|
|
823
|
+
}
|
|
824
|
+
parseBackground(root) {
|
|
825
|
+
let bgPr;
|
|
826
|
+
for (const bg of descendants(root, "bg")) {
|
|
827
|
+
const inner = el(bg, "bgPr");
|
|
828
|
+
if (inner) {
|
|
829
|
+
bgPr = inner;
|
|
830
|
+
break;
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
if (!bgPr) {
|
|
834
|
+
return null;
|
|
835
|
+
}
|
|
836
|
+
const solidFill = descendant(bgPr, "solidFill");
|
|
837
|
+
const solid = solidFill ? descendant(solidFill, "srgbClr") : void 0;
|
|
838
|
+
if (solid) {
|
|
839
|
+
const hex = at(solid, "val") ?? "";
|
|
840
|
+
if (hex !== "") {
|
|
841
|
+
return { color: "#" + hex };
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
const grad = descendant(bgPr, "gradFill");
|
|
845
|
+
if (grad) {
|
|
846
|
+
const css = this.gradFillToCss(grad);
|
|
847
|
+
if (css !== null) {
|
|
848
|
+
return { gradient: css };
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
const blipFill = descendant(bgPr, "blipFill");
|
|
852
|
+
const blip = blipFill ? descendant(blipFill, "blip") : void 0;
|
|
853
|
+
if (blip) {
|
|
854
|
+
const rid = at(blip, "embed") ?? "";
|
|
855
|
+
if (rid !== "" && this.currentSlideRels[rid] !== void 0) {
|
|
856
|
+
const dataUri = this.readMediaAsDataUri(this.currentSlideRels[rid].target);
|
|
857
|
+
if (dataUri !== null) {
|
|
858
|
+
return { image: dataUri };
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
return null;
|
|
863
|
+
}
|
|
864
|
+
gradFillToCss(grad) {
|
|
865
|
+
const stops = descendants(grad, "gs");
|
|
866
|
+
if (stops.length === 0) {
|
|
867
|
+
return null;
|
|
868
|
+
}
|
|
869
|
+
const stopStrings = [];
|
|
870
|
+
for (const stop of stops) {
|
|
871
|
+
const pos = parseInt(at(stop, "pos") ?? "0", 10) || 0;
|
|
872
|
+
const pct = round1(pos / 1e3);
|
|
873
|
+
const color = descendant(stop, "srgbClr");
|
|
874
|
+
if (!color) {
|
|
875
|
+
continue;
|
|
876
|
+
}
|
|
877
|
+
const hex = at(color, "val") ?? "";
|
|
878
|
+
if (hex === "") {
|
|
879
|
+
continue;
|
|
880
|
+
}
|
|
881
|
+
stopStrings.push("#" + hex.toLowerCase() + " " + numToStr(pct) + "%");
|
|
882
|
+
}
|
|
883
|
+
if (stopStrings.length === 0) {
|
|
884
|
+
return null;
|
|
885
|
+
}
|
|
886
|
+
const lin = descendant(grad, "lin");
|
|
887
|
+
let angle = 180;
|
|
888
|
+
if (lin) {
|
|
889
|
+
const pptxAng = parseInt(at(lin, "ang") ?? "0", 10) || 0;
|
|
890
|
+
const deg = pptxAng / 6e4 + 90;
|
|
891
|
+
angle = Math.round((deg % 360 + 360) % 360);
|
|
892
|
+
}
|
|
893
|
+
return "linear-gradient(" + angle + "deg, " + stopStrings.join(", ") + ")";
|
|
894
|
+
}
|
|
895
|
+
readMediaAsDataUri(archivePath) {
|
|
896
|
+
const part = this.parts[archivePath];
|
|
897
|
+
if (part === void 0) {
|
|
898
|
+
return null;
|
|
899
|
+
}
|
|
900
|
+
const mime = this.guessMimeFromArchivePath(archivePath);
|
|
901
|
+
return "data:" + mime + ";base64," + base64Encode(part);
|
|
902
|
+
}
|
|
903
|
+
guessMimeFromArchivePath(path) {
|
|
904
|
+
switch (extension(path)) {
|
|
905
|
+
case "png":
|
|
906
|
+
return "image/png";
|
|
907
|
+
case "jpg":
|
|
908
|
+
case "jpeg":
|
|
909
|
+
return "image/jpeg";
|
|
910
|
+
case "gif":
|
|
911
|
+
return "image/gif";
|
|
912
|
+
case "svg":
|
|
913
|
+
return "image/svg+xml";
|
|
914
|
+
case "webp":
|
|
915
|
+
return "image/webp";
|
|
916
|
+
default:
|
|
917
|
+
return "application/octet-stream";
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
parseShape(sp) {
|
|
921
|
+
const xfrm = descendant(sp, "xfrm");
|
|
922
|
+
if (!xfrm) {
|
|
923
|
+
return null;
|
|
924
|
+
}
|
|
925
|
+
const offset = el(xfrm, "off");
|
|
926
|
+
const extent = el(xfrm, "ext");
|
|
927
|
+
if (!offset || !extent) {
|
|
928
|
+
return null;
|
|
929
|
+
}
|
|
930
|
+
const x = parseInt(at(offset, "x") ?? "0", 10) || 0;
|
|
931
|
+
const y = parseInt(at(offset, "y") ?? "0", 10) || 0;
|
|
932
|
+
const cx = parseInt(at(extent, "cx") ?? "0", 10) || 0;
|
|
933
|
+
const cy = parseInt(at(extent, "cy") ?? "0", 10) || 0;
|
|
934
|
+
const cNvPr = descendant(sp, "cNvPr");
|
|
935
|
+
const base = {
|
|
936
|
+
id: cNvPr ? at(cNvPr, "name") ?? "imported-" + randInt(1e3, 9999) : "imported-" + randInt(1e3, 9999),
|
|
937
|
+
x: Emu.toFracX(x),
|
|
938
|
+
y: Emu.toFracY(y),
|
|
939
|
+
w: Emu.toFracX(cx),
|
|
940
|
+
h: Emu.toFracY(cy)
|
|
941
|
+
};
|
|
942
|
+
const tBody = descendant(sp, "txBody");
|
|
943
|
+
const paragraphMarkdown = [];
|
|
944
|
+
let anyDecoration = false;
|
|
945
|
+
if (tBody) {
|
|
946
|
+
for (const p of descendants(tBody, "p")) {
|
|
947
|
+
const [md, decorated] = this.paragraphToMarkdown(p);
|
|
948
|
+
paragraphMarkdown.push(md);
|
|
949
|
+
anyDecoration = anyDecoration || decorated;
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
const prstGeom = descendant(sp, "prstGeom");
|
|
953
|
+
const prst = prstGeom ? at(prstGeom, "prst") ?? null : null;
|
|
954
|
+
const hasText = paragraphMarkdown.some((t) => t !== "");
|
|
955
|
+
if (hasText) {
|
|
956
|
+
return {
|
|
957
|
+
...base,
|
|
958
|
+
type: "text",
|
|
959
|
+
content: paragraphMarkdown.join("\n"),
|
|
960
|
+
format: anyDecoration ? "markdown" : "plain"
|
|
961
|
+
};
|
|
962
|
+
}
|
|
963
|
+
let shapeKind;
|
|
964
|
+
switch (prst) {
|
|
965
|
+
case "rect":
|
|
966
|
+
shapeKind = "rect";
|
|
967
|
+
break;
|
|
968
|
+
case "roundRect":
|
|
969
|
+
shapeKind = "rounded-rect";
|
|
970
|
+
break;
|
|
971
|
+
case "ellipse":
|
|
972
|
+
shapeKind = "ellipse";
|
|
973
|
+
break;
|
|
974
|
+
case "triangle":
|
|
975
|
+
shapeKind = "triangle";
|
|
976
|
+
break;
|
|
977
|
+
case "line":
|
|
978
|
+
shapeKind = "line";
|
|
979
|
+
break;
|
|
980
|
+
case "rightArrow":
|
|
981
|
+
shapeKind = "arrow";
|
|
982
|
+
break;
|
|
983
|
+
default:
|
|
984
|
+
shapeKind = null;
|
|
985
|
+
}
|
|
986
|
+
if (shapeKind !== null) {
|
|
987
|
+
return { ...base, type: "shape", shape: shapeKind };
|
|
988
|
+
}
|
|
989
|
+
return null;
|
|
990
|
+
}
|
|
991
|
+
parsePic(pic) {
|
|
992
|
+
const xfrm = descendant(pic, "xfrm");
|
|
993
|
+
if (!xfrm) {
|
|
994
|
+
return null;
|
|
995
|
+
}
|
|
996
|
+
const offset = el(xfrm, "off");
|
|
997
|
+
const extent = el(xfrm, "ext");
|
|
998
|
+
if (!offset || !extent) {
|
|
999
|
+
return null;
|
|
1000
|
+
}
|
|
1001
|
+
let src = "";
|
|
1002
|
+
const blip = descendant(pic, "blip");
|
|
1003
|
+
if (blip) {
|
|
1004
|
+
const rid = at(blip, "embed") ?? "";
|
|
1005
|
+
if (rid !== "" && this.currentSlideRels[rid] !== void 0) {
|
|
1006
|
+
const dataUri = this.readMediaAsDataUri(this.currentSlideRels[rid].target);
|
|
1007
|
+
if (dataUri !== null) {
|
|
1008
|
+
src = dataUri;
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
const cNvPr = descendant(pic, "cNvPr");
|
|
1013
|
+
return {
|
|
1014
|
+
id: cNvPr ? at(cNvPr, "name") ?? "imported-" + randInt(1e3, 9999) : "imported-" + randInt(1e3, 9999),
|
|
1015
|
+
type: "image",
|
|
1016
|
+
x: Emu.toFracX(parseInt(at(offset, "x") ?? "0", 10) || 0),
|
|
1017
|
+
y: Emu.toFracY(parseInt(at(offset, "y") ?? "0", 10) || 0),
|
|
1018
|
+
w: Emu.toFracX(parseInt(at(extent, "cx") ?? "0", 10) || 0),
|
|
1019
|
+
h: Emu.toFracY(parseInt(at(extent, "cy") ?? "0", 10) || 0),
|
|
1020
|
+
src,
|
|
1021
|
+
fit: "contain"
|
|
1022
|
+
};
|
|
1023
|
+
}
|
|
1024
|
+
parseGraphicFrame(gf) {
|
|
1025
|
+
const xfrm = descendant(gf, "xfrm");
|
|
1026
|
+
if (!xfrm) {
|
|
1027
|
+
return null;
|
|
1028
|
+
}
|
|
1029
|
+
const offset = el(xfrm, "off");
|
|
1030
|
+
const extent = el(xfrm, "ext");
|
|
1031
|
+
if (!offset || !extent) {
|
|
1032
|
+
return null;
|
|
1033
|
+
}
|
|
1034
|
+
const tbl = descendant(gf, "tbl");
|
|
1035
|
+
if (!tbl) {
|
|
1036
|
+
return null;
|
|
1037
|
+
}
|
|
1038
|
+
const rows = descendants(tbl, "tr");
|
|
1039
|
+
if (rows.length === 0) {
|
|
1040
|
+
return null;
|
|
1041
|
+
}
|
|
1042
|
+
const headerCells = descendants(rows[0], "tc");
|
|
1043
|
+
const columns = [];
|
|
1044
|
+
headerCells.forEach((cell, i) => {
|
|
1045
|
+
const label = this.cellText(cell);
|
|
1046
|
+
columns.push({ key: "col" + (i + 1), label });
|
|
1047
|
+
});
|
|
1048
|
+
const bodyRows = [];
|
|
1049
|
+
for (let r = 1; r < rows.length; r++) {
|
|
1050
|
+
const rowCells = descendants(rows[r], "tc");
|
|
1051
|
+
const rowData = {};
|
|
1052
|
+
columns.forEach((col, i) => {
|
|
1053
|
+
const cell = rowCells[i];
|
|
1054
|
+
if (cell !== void 0) {
|
|
1055
|
+
rowData[col.key] = this.cellText(cell);
|
|
1056
|
+
}
|
|
1057
|
+
});
|
|
1058
|
+
bodyRows.push(rowData);
|
|
1059
|
+
}
|
|
1060
|
+
const cNvPr = descendant(gf, "cNvPr");
|
|
1061
|
+
return {
|
|
1062
|
+
id: cNvPr ? at(cNvPr, "name") ?? "imported-table-" + randInt(1e3, 9999) : "imported-table-" + randInt(1e3, 9999),
|
|
1063
|
+
type: "table",
|
|
1064
|
+
x: Emu.toFracX(parseInt(at(offset, "x") ?? "0", 10) || 0),
|
|
1065
|
+
y: Emu.toFracY(parseInt(at(offset, "y") ?? "0", 10) || 0),
|
|
1066
|
+
w: Emu.toFracX(parseInt(at(extent, "cx") ?? "0", 10) || 0),
|
|
1067
|
+
h: Emu.toFracY(parseInt(at(extent, "cy") ?? "0", 10) || 0),
|
|
1068
|
+
columns,
|
|
1069
|
+
rows: bodyRows
|
|
1070
|
+
};
|
|
1071
|
+
}
|
|
1072
|
+
cellText(cell) {
|
|
1073
|
+
const segments = [];
|
|
1074
|
+
for (const t of descendants(cell, "t")) {
|
|
1075
|
+
segments.push(deepText(t));
|
|
1076
|
+
}
|
|
1077
|
+
return segments.join("");
|
|
1078
|
+
}
|
|
1079
|
+
paragraphToMarkdown(p) {
|
|
1080
|
+
const pPr = descendant(p, "pPr");
|
|
1081
|
+
const bu = pPr ? descendant(pPr, "buChar") : void 0;
|
|
1082
|
+
const isBullet = bu !== void 0;
|
|
1083
|
+
const runs = descendants(p, "r");
|
|
1084
|
+
if (runs.length === 0) {
|
|
1085
|
+
return [isBullet ? "- " : "", isBullet];
|
|
1086
|
+
}
|
|
1087
|
+
const parsed = [];
|
|
1088
|
+
let allBold = true;
|
|
1089
|
+
let allItalic = true;
|
|
1090
|
+
let anyNonEmpty = false;
|
|
1091
|
+
for (const r of runs) {
|
|
1092
|
+
const rPr = el(r, "rPr");
|
|
1093
|
+
const tNode = el(r, "t");
|
|
1094
|
+
const text = tNode ? deepText(tNode) : "";
|
|
1095
|
+
let b = false;
|
|
1096
|
+
let i = false;
|
|
1097
|
+
let code = false;
|
|
1098
|
+
if (rPr) {
|
|
1099
|
+
b = (at(rPr, "b") ?? "0") === "1";
|
|
1100
|
+
i = (at(rPr, "i") ?? "0") === "1";
|
|
1101
|
+
const latin = el(rPr, "latin");
|
|
1102
|
+
if (latin) {
|
|
1103
|
+
const typeface = (at(latin, "typeface") ?? "").toLowerCase();
|
|
1104
|
+
if (typeface.includes("consola") || typeface.includes("mono") || typeface.includes("courier")) {
|
|
1105
|
+
code = true;
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
if (text !== "") {
|
|
1110
|
+
anyNonEmpty = true;
|
|
1111
|
+
if (!b) {
|
|
1112
|
+
allBold = false;
|
|
1113
|
+
}
|
|
1114
|
+
if (!i) {
|
|
1115
|
+
allItalic = false;
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
parsed.push({ text, b, i, code });
|
|
1119
|
+
}
|
|
1120
|
+
if (!anyNonEmpty) {
|
|
1121
|
+
return [isBullet ? "- " : "", isBullet];
|
|
1122
|
+
}
|
|
1123
|
+
let line = "";
|
|
1124
|
+
let anyDecoration = false;
|
|
1125
|
+
for (const run of parsed) {
|
|
1126
|
+
const text = run.text;
|
|
1127
|
+
const emitBold = run.b && !allBold;
|
|
1128
|
+
const emitItalic = run.i && !allItalic;
|
|
1129
|
+
if (run.code) {
|
|
1130
|
+
line += "`" + text + "`";
|
|
1131
|
+
anyDecoration = true;
|
|
1132
|
+
} else if (emitBold && emitItalic) {
|
|
1133
|
+
line += "***" + text + "***";
|
|
1134
|
+
anyDecoration = true;
|
|
1135
|
+
} else if (emitBold) {
|
|
1136
|
+
line += "**" + text + "**";
|
|
1137
|
+
anyDecoration = true;
|
|
1138
|
+
} else if (emitItalic) {
|
|
1139
|
+
line += "*" + text + "*";
|
|
1140
|
+
anyDecoration = true;
|
|
1141
|
+
} else {
|
|
1142
|
+
line += text;
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
if (isBullet) {
|
|
1146
|
+
return ["- " + line, true];
|
|
1147
|
+
}
|
|
1148
|
+
return [line, anyDecoration];
|
|
1149
|
+
}
|
|
1150
|
+
};
|
|
1151
|
+
function randInt(min, max) {
|
|
1152
|
+
return Math.floor(Math.random() * (max - min + 1)) + min;
|
|
1153
|
+
}
|
|
1154
|
+
function round1(x) {
|
|
1155
|
+
return Math.round(x * 10) / 10;
|
|
1156
|
+
}
|
|
1157
|
+
function numToStr(n) {
|
|
1158
|
+
return String(n);
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
// src/util.ts
|
|
1162
|
+
function isPlainObject(value) {
|
|
1163
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1164
|
+
}
|
|
1165
|
+
function isNumeric(v) {
|
|
1166
|
+
if (typeof v === "number") return Number.isFinite(v);
|
|
1167
|
+
if (typeof v !== "string") return false;
|
|
1168
|
+
return /^\s*[+-]?(\d+(\.\d+)?|\.\d+)([eE][+-]?\d+)?$/.test(v);
|
|
1169
|
+
}
|
|
1170
|
+
function gettype(v) {
|
|
1171
|
+
if (v === null || v === void 0) return "null";
|
|
1172
|
+
if (Array.isArray(v)) return "array";
|
|
1173
|
+
switch (typeof v) {
|
|
1174
|
+
case "object":
|
|
1175
|
+
return "object";
|
|
1176
|
+
case "boolean":
|
|
1177
|
+
return "boolean";
|
|
1178
|
+
case "number":
|
|
1179
|
+
return Number.isInteger(v) ? "integer" : "double";
|
|
1180
|
+
case "string":
|
|
1181
|
+
return "string";
|
|
1182
|
+
default:
|
|
1183
|
+
return typeof v;
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
function clone(v) {
|
|
1187
|
+
return typeof structuredClone === "function" ? structuredClone(v) : JSON.parse(JSON.stringify(v));
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
// src/schema/schema.ts
|
|
1191
|
+
var Schema = {
|
|
1192
|
+
VERSION: "0.1.0",
|
|
1193
|
+
ELEMENT_TYPES: ["text", "image", "chart", "code", "table", "shape", "embed"],
|
|
1194
|
+
SLIDE_LAYOUTS: [
|
|
1195
|
+
"blank",
|
|
1196
|
+
"title",
|
|
1197
|
+
"title-content",
|
|
1198
|
+
"two-column",
|
|
1199
|
+
"section-divider",
|
|
1200
|
+
"image-text",
|
|
1201
|
+
"text-image",
|
|
1202
|
+
"quote"
|
|
1203
|
+
],
|
|
1204
|
+
SHAPE_KINDS: ["rect", "rounded-rect", "ellipse", "triangle", "line", "arrow"],
|
|
1205
|
+
TEXT_FORMATS: ["markdown", "html", "plain"],
|
|
1206
|
+
SLIDE_TRANSITION_KINDS: ["none", "fade", "slide", "zoom"],
|
|
1207
|
+
SLIDE_TRANSITION_DIRECTIONS: ["left", "right", "up", "down"],
|
|
1208
|
+
ANIMATION_EFFECTS: ["fade", "fly-in", "zoom", "wipe"],
|
|
1209
|
+
ANIMATION_TRIGGERS: ["on-click", "with-prev", "after-prev"],
|
|
1210
|
+
ANIMATION_DIRECTIONS: ["left", "right", "up", "down"],
|
|
1211
|
+
ANIMATION_DEFAULT_DURATION_MS: 500,
|
|
1212
|
+
DEFAULT_SLIDE_WIDTH_EMU: 9144e3,
|
|
1213
|
+
DEFAULT_SLIDE_HEIGHT_EMU: 5143500,
|
|
1214
|
+
DEFAULT_THEME_NAME: "default",
|
|
1215
|
+
deckRequiredKeys() {
|
|
1216
|
+
return ["id", "title", "slides", "theme"];
|
|
1217
|
+
},
|
|
1218
|
+
slideRequiredKeys() {
|
|
1219
|
+
return ["id", "elements"];
|
|
1220
|
+
},
|
|
1221
|
+
elementRequiredKeys() {
|
|
1222
|
+
return ["id", "type", "x", "y", "w", "h"];
|
|
1223
|
+
},
|
|
1224
|
+
jsonSchema() {
|
|
1225
|
+
return {
|
|
1226
|
+
$schema: "http://json-schema.org/draft-07/schema#",
|
|
1227
|
+
title: "DarkSlide Deck",
|
|
1228
|
+
type: "object",
|
|
1229
|
+
required: this.deckRequiredKeys(),
|
|
1230
|
+
properties: {
|
|
1231
|
+
id: { type: "string" },
|
|
1232
|
+
title: { type: "string" },
|
|
1233
|
+
theme: {
|
|
1234
|
+
type: "object",
|
|
1235
|
+
required: ["name"],
|
|
1236
|
+
properties: {
|
|
1237
|
+
name: { type: "string" },
|
|
1238
|
+
aspectRatio: { type: "number" },
|
|
1239
|
+
slideWidth: { type: "number" },
|
|
1240
|
+
colors: {
|
|
1241
|
+
type: "object",
|
|
1242
|
+
properties: {
|
|
1243
|
+
background: { type: "string" },
|
|
1244
|
+
text: { type: "string" },
|
|
1245
|
+
muted: { type: "string" },
|
|
1246
|
+
accent: { type: "string" },
|
|
1247
|
+
surface: { type: "string" }
|
|
1248
|
+
}
|
|
1249
|
+
},
|
|
1250
|
+
fonts: {
|
|
1251
|
+
type: "object",
|
|
1252
|
+
properties: {
|
|
1253
|
+
heading: { type: "string" },
|
|
1254
|
+
body: { type: "string" },
|
|
1255
|
+
mono: { type: "string" }
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
},
|
|
1260
|
+
slides: {
|
|
1261
|
+
type: "array",
|
|
1262
|
+
items: {
|
|
1263
|
+
type: "object",
|
|
1264
|
+
required: this.slideRequiredKeys(),
|
|
1265
|
+
properties: {
|
|
1266
|
+
id: { type: "string" },
|
|
1267
|
+
layout: { type: "string", enum: this.SLIDE_LAYOUTS },
|
|
1268
|
+
elements: { type: "array", items: elementJsonSchema() },
|
|
1269
|
+
background: {
|
|
1270
|
+
type: "object",
|
|
1271
|
+
properties: {
|
|
1272
|
+
color: { type: "string" },
|
|
1273
|
+
image: { type: "string" },
|
|
1274
|
+
imageFit: { type: "string", enum: ["contain", "cover", "fill"] },
|
|
1275
|
+
gradient: { type: "string" }
|
|
1276
|
+
}
|
|
1277
|
+
},
|
|
1278
|
+
transition: {
|
|
1279
|
+
type: "object",
|
|
1280
|
+
properties: {
|
|
1281
|
+
kind: { type: "string", enum: this.SLIDE_TRANSITION_KINDS },
|
|
1282
|
+
duration: { type: "number" },
|
|
1283
|
+
direction: { type: "string", enum: this.SLIDE_TRANSITION_DIRECTIONS }
|
|
1284
|
+
}
|
|
1285
|
+
},
|
|
1286
|
+
notes: { type: "string" },
|
|
1287
|
+
metadata: { type: "object" }
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
},
|
|
1291
|
+
metadata: { type: "object" }
|
|
1292
|
+
}
|
|
1293
|
+
};
|
|
1294
|
+
}
|
|
1295
|
+
};
|
|
1296
|
+
function elementJsonSchema() {
|
|
1297
|
+
return {
|
|
1298
|
+
type: "object",
|
|
1299
|
+
required: Schema.elementRequiredKeys(),
|
|
1300
|
+
properties: {
|
|
1301
|
+
id: { type: "string" },
|
|
1302
|
+
type: { type: "string", enum: Schema.ELEMENT_TYPES },
|
|
1303
|
+
x: { type: "number", minimum: 0, maximum: 1 },
|
|
1304
|
+
y: { type: "number", minimum: 0, maximum: 1 },
|
|
1305
|
+
w: { type: "number", minimum: 0, maximum: 1 },
|
|
1306
|
+
h: { type: "number", minimum: 0, maximum: 1 },
|
|
1307
|
+
rotation: { type: "number" },
|
|
1308
|
+
z: { type: "integer" },
|
|
1309
|
+
locked: { type: "boolean" },
|
|
1310
|
+
hidden: { type: "boolean" },
|
|
1311
|
+
href: { type: "string" },
|
|
1312
|
+
content: { type: "string" },
|
|
1313
|
+
format: { type: "string", enum: Schema.TEXT_FORMATS },
|
|
1314
|
+
style: { type: "object" },
|
|
1315
|
+
src: { type: "string" },
|
|
1316
|
+
alt: { type: "string" },
|
|
1317
|
+
fit: { type: "string", enum: ["contain", "cover", "fill", "scale-down"] },
|
|
1318
|
+
shape: { type: "string", enum: Schema.SHAPE_KINDS },
|
|
1319
|
+
fill: { type: "string" },
|
|
1320
|
+
stroke: { type: "string" },
|
|
1321
|
+
strokeWidth: { type: "number" },
|
|
1322
|
+
dashed: { type: "boolean" },
|
|
1323
|
+
radius: { type: "number" },
|
|
1324
|
+
code: { type: "string" },
|
|
1325
|
+
language: { type: "string" },
|
|
1326
|
+
codeTheme: { type: "string" },
|
|
1327
|
+
columns: { type: "array" },
|
|
1328
|
+
rows: { type: "array" },
|
|
1329
|
+
option: { type: "object" },
|
|
1330
|
+
chartTheme: { type: "string" },
|
|
1331
|
+
animation: {
|
|
1332
|
+
type: "object",
|
|
1333
|
+
required: ["effect"],
|
|
1334
|
+
properties: {
|
|
1335
|
+
effect: { type: "string", enum: Schema.ANIMATION_EFFECTS },
|
|
1336
|
+
trigger: { type: "string", enum: Schema.ANIMATION_TRIGGERS },
|
|
1337
|
+
direction: { type: "string", enum: Schema.ANIMATION_DIRECTIONS },
|
|
1338
|
+
duration: { type: "number" },
|
|
1339
|
+
delay: { type: "number" },
|
|
1340
|
+
order: { type: "number" },
|
|
1341
|
+
byParagraph: { type: "boolean" }
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
};
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
// src/schema/repairer.ts
|
|
1349
|
+
var Repairer = class {
|
|
1350
|
+
constructor() {
|
|
1351
|
+
this.idCounter = 0;
|
|
1352
|
+
}
|
|
1353
|
+
repair(deckInput) {
|
|
1354
|
+
this.idCounter = 0;
|
|
1355
|
+
const deck = isPlainObject(deckInput) ? clone(deckInput) : {};
|
|
1356
|
+
deck.id ?? (deck.id = this.generateId("deck"));
|
|
1357
|
+
deck.title ?? (deck.title = "Untitled");
|
|
1358
|
+
deck.theme = this.repairTheme(deck.theme ?? null);
|
|
1359
|
+
deck.slides = this.repairSlides(deck.slides ?? []);
|
|
1360
|
+
return deck;
|
|
1361
|
+
}
|
|
1362
|
+
repairTheme(theme) {
|
|
1363
|
+
if (!isPlainObject(theme)) return { name: Schema.DEFAULT_THEME_NAME };
|
|
1364
|
+
theme.name ?? (theme.name = Schema.DEFAULT_THEME_NAME);
|
|
1365
|
+
return theme;
|
|
1366
|
+
}
|
|
1367
|
+
repairSlides(slides) {
|
|
1368
|
+
if (!Array.isArray(slides)) return [];
|
|
1369
|
+
const out = [];
|
|
1370
|
+
slides.forEach((slide, i) => {
|
|
1371
|
+
if (!isPlainObject(slide)) return;
|
|
1372
|
+
out.push(this.repairSlide(slide, i));
|
|
1373
|
+
});
|
|
1374
|
+
return out;
|
|
1375
|
+
}
|
|
1376
|
+
repairSlide(slide, index) {
|
|
1377
|
+
slide.id ?? (slide.id = this.generateId("s", index));
|
|
1378
|
+
slide.elements = this.repairElements(slide.elements ?? []);
|
|
1379
|
+
if (slide.layout !== void 0 && !Schema.SLIDE_LAYOUTS.includes(slide.layout)) {
|
|
1380
|
+
slide.layout = "blank";
|
|
1381
|
+
}
|
|
1382
|
+
return slide;
|
|
1383
|
+
}
|
|
1384
|
+
repairElements(elements) {
|
|
1385
|
+
if (!Array.isArray(elements)) return [];
|
|
1386
|
+
const out = [];
|
|
1387
|
+
elements.forEach((element, i) => {
|
|
1388
|
+
if (!isPlainObject(element)) return;
|
|
1389
|
+
const repaired = this.repairElement(element, i);
|
|
1390
|
+
if (repaired !== null) out.push(repaired);
|
|
1391
|
+
});
|
|
1392
|
+
return out;
|
|
1393
|
+
}
|
|
1394
|
+
repairElement(element, index) {
|
|
1395
|
+
if (element.type === void 0 || !Schema.ELEMENT_TYPES.includes(element.type)) {
|
|
1396
|
+
return null;
|
|
1397
|
+
}
|
|
1398
|
+
element.id ?? (element.id = this.generateId("e", index));
|
|
1399
|
+
for (const coord of ["x", "y", "w", "h"]) {
|
|
1400
|
+
element[coord] = this.clamp(isNumeric(element[coord]) ? Number(element[coord]) : 0, 0, 1);
|
|
1401
|
+
}
|
|
1402
|
+
if (element.w < 0.02) element.w = 0.02;
|
|
1403
|
+
if (element.h < 0.02) element.h = 0.02;
|
|
1404
|
+
switch (element.type) {
|
|
1405
|
+
case "text":
|
|
1406
|
+
element.content = typeof element.content === "string" ? element.content : "";
|
|
1407
|
+
break;
|
|
1408
|
+
case "image":
|
|
1409
|
+
element.src = typeof element.src === "string" ? element.src : "";
|
|
1410
|
+
break;
|
|
1411
|
+
case "shape":
|
|
1412
|
+
element.shape = typeof element.shape === "string" && Schema.SHAPE_KINDS.includes(element.shape) ? element.shape : "rect";
|
|
1413
|
+
break;
|
|
1414
|
+
case "code":
|
|
1415
|
+
element.code = typeof element.code === "string" ? element.code : "";
|
|
1416
|
+
break;
|
|
1417
|
+
}
|
|
1418
|
+
return element;
|
|
1419
|
+
}
|
|
1420
|
+
generateId(prefix, index = 0) {
|
|
1421
|
+
this.idCounter++;
|
|
1422
|
+
const t = (Math.floor(Date.now() / 1e3) & 16777215).toString(16);
|
|
1423
|
+
const n = String(this.idCounter + index).padStart(3, "0");
|
|
1424
|
+
return `${prefix}-${t}-${n}`;
|
|
1425
|
+
}
|
|
1426
|
+
clamp(v, min, max) {
|
|
1427
|
+
if (!Number.isFinite(v)) return min;
|
|
1428
|
+
return Math.max(min, Math.min(max, v));
|
|
1429
|
+
}
|
|
1430
|
+
};
|
|
1431
|
+
|
|
1432
|
+
// src/schema/validator.ts
|
|
1433
|
+
var Validator = class {
|
|
1434
|
+
validate(deck) {
|
|
1435
|
+
const errors = [];
|
|
1436
|
+
const d = isPlainObject(deck) ? deck : {};
|
|
1437
|
+
for (const key of Schema.deckRequiredKeys()) {
|
|
1438
|
+
if (!(key in d)) errors.push(err(`/${key}`, key, "missing", null, `Deck must have an \`${key}\` field.`));
|
|
1439
|
+
}
|
|
1440
|
+
if (d.id !== void 0 && typeof d.id !== "string") {
|
|
1441
|
+
errors.push(err("/id", "string", gettype(d.id), d.id, "Deck id must be a string."));
|
|
1442
|
+
}
|
|
1443
|
+
if (d.title !== void 0 && typeof d.title !== "string") {
|
|
1444
|
+
errors.push(err("/title", "string", gettype(d.title), d.title, "Deck title must be a string."));
|
|
1445
|
+
}
|
|
1446
|
+
if (d.theme !== void 0) {
|
|
1447
|
+
if (!isPlainObject(d.theme)) {
|
|
1448
|
+
errors.push(err("/theme", "object", gettype(d.theme), d.theme, "Theme must be an object with at least a `name` field."));
|
|
1449
|
+
} else if (d.theme.name === void 0) {
|
|
1450
|
+
errors.push(err("/theme/name", "string", "missing", null, "Theme must have a name."));
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
if (d.slides !== void 0) {
|
|
1454
|
+
if (!Array.isArray(d.slides)) {
|
|
1455
|
+
errors.push(err("/slides", "array", gettype(d.slides), d.slides, "Slides must be a JSON array."));
|
|
1456
|
+
} else {
|
|
1457
|
+
d.slides.forEach((slide, i) => {
|
|
1458
|
+
errors.push(...this.validateSlide(slide, `/slides/${i}`));
|
|
1459
|
+
});
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
return errors;
|
|
1463
|
+
}
|
|
1464
|
+
validateSlide(slide, path) {
|
|
1465
|
+
const errors = [];
|
|
1466
|
+
if (!isPlainObject(slide)) {
|
|
1467
|
+
return [err(path, "object", gettype(slide), slide, "Each slide must be a JSON object.")];
|
|
1468
|
+
}
|
|
1469
|
+
for (const key of Schema.slideRequiredKeys()) {
|
|
1470
|
+
if (!(key in slide)) errors.push(err(`${path}/${key}`, key, "missing", null, `Slide must have a \`${key}\` field.`));
|
|
1471
|
+
}
|
|
1472
|
+
if (slide.id !== void 0 && typeof slide.id !== "string") {
|
|
1473
|
+
errors.push(err(`${path}/id`, "string", gettype(slide.id), slide.id, "Slide id must be a string."));
|
|
1474
|
+
}
|
|
1475
|
+
if (slide.elements !== void 0) {
|
|
1476
|
+
if (!Array.isArray(slide.elements)) {
|
|
1477
|
+
errors.push(err(`${path}/elements`, "array", gettype(slide.elements), slide.elements, "Slide elements must be an array."));
|
|
1478
|
+
} else {
|
|
1479
|
+
slide.elements.forEach((element, i) => {
|
|
1480
|
+
errors.push(...this.validateElement(element, `${path}/elements/${i}`));
|
|
1481
|
+
});
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
if (slide.notes !== void 0 && typeof slide.notes !== "string") {
|
|
1485
|
+
errors.push(err(`${path}/notes`, "string", gettype(slide.notes), slide.notes, "Slide notes must be a string."));
|
|
1486
|
+
}
|
|
1487
|
+
return errors;
|
|
1488
|
+
}
|
|
1489
|
+
validateElement(element, path) {
|
|
1490
|
+
const errors = [];
|
|
1491
|
+
if (!isPlainObject(element)) {
|
|
1492
|
+
return [err(path, "object", gettype(element), element, "Each element must be a JSON object.")];
|
|
1493
|
+
}
|
|
1494
|
+
for (const key of Schema.elementRequiredKeys()) {
|
|
1495
|
+
if (!(key in element)) errors.push(err(`${path}/${key}`, key, "missing", null, `Element must have a \`${key}\` field.`));
|
|
1496
|
+
}
|
|
1497
|
+
if (element.type !== void 0 && !Schema.ELEMENT_TYPES.includes(element.type)) {
|
|
1498
|
+
errors.push(
|
|
1499
|
+
err(
|
|
1500
|
+
`${path}/type`,
|
|
1501
|
+
"one of: " + Schema.ELEMENT_TYPES.join(" / "),
|
|
1502
|
+
String(element.type),
|
|
1503
|
+
element.type,
|
|
1504
|
+
"Unknown element type \u2014 supported: " + Schema.ELEMENT_TYPES.join(", ")
|
|
1505
|
+
)
|
|
1506
|
+
);
|
|
1507
|
+
}
|
|
1508
|
+
for (const coord of ["x", "y", "w", "h"]) {
|
|
1509
|
+
if (!(coord in element)) continue;
|
|
1510
|
+
if (!isNumeric(element[coord])) {
|
|
1511
|
+
errors.push(
|
|
1512
|
+
err(
|
|
1513
|
+
`${path}/${coord}`,
|
|
1514
|
+
"number (0..1)",
|
|
1515
|
+
gettype(element[coord]),
|
|
1516
|
+
element[coord],
|
|
1517
|
+
`Element ${coord} must be a number in the 0..1 range (slide-relative fraction).`
|
|
1518
|
+
)
|
|
1519
|
+
);
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
if (typeof element.type === "string") {
|
|
1523
|
+
switch (element.type) {
|
|
1524
|
+
case "text":
|
|
1525
|
+
if (typeof element.content !== "string") {
|
|
1526
|
+
errors.push(err(`${path}/content`, "string", gettype(element.content ?? null), element.content ?? null, "Text element must have a `content` string."));
|
|
1527
|
+
}
|
|
1528
|
+
break;
|
|
1529
|
+
case "image":
|
|
1530
|
+
if (typeof element.src !== "string") {
|
|
1531
|
+
errors.push(err(`${path}/src`, "string (URL or data URI)", gettype(element.src ?? null), element.src ?? null, "Image element must have a `src` string."));
|
|
1532
|
+
}
|
|
1533
|
+
break;
|
|
1534
|
+
case "shape":
|
|
1535
|
+
if (!Schema.SHAPE_KINDS.includes(element.shape)) {
|
|
1536
|
+
errors.push(err(`${path}/shape`, "one of: " + Schema.SHAPE_KINDS.join(" / "), String(element.shape ?? "missing"), element.shape ?? null, "Shape element must specify a known `shape` kind."));
|
|
1537
|
+
}
|
|
1538
|
+
break;
|
|
1539
|
+
case "code":
|
|
1540
|
+
if (typeof element.code !== "string") {
|
|
1541
|
+
errors.push(err(`${path}/code`, "string", gettype(element.code ?? null), element.code ?? null, "Code element must have a `code` string."));
|
|
1542
|
+
}
|
|
1543
|
+
break;
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
return errors;
|
|
1547
|
+
}
|
|
1548
|
+
};
|
|
1549
|
+
function err(path, expected, got, value, hint) {
|
|
1550
|
+
return { path, expected, got, value, hint };
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
// src/helpers/chart-translator.ts
|
|
1554
|
+
var SUPPORTED_TYPES = ["bar", "line", "pie", "scatter"];
|
|
1555
|
+
var isScalar = (v) => typeof v === "number" || typeof v === "string" || typeof v === "boolean";
|
|
1556
|
+
var ChartTranslator = {
|
|
1557
|
+
SUPPORTED_TYPES,
|
|
1558
|
+
translate(option) {
|
|
1559
|
+
const rawSeries = extractSeries(option);
|
|
1560
|
+
if (rawSeries.length === 0) return null;
|
|
1561
|
+
let categories = extractCategories(option);
|
|
1562
|
+
const series = [];
|
|
1563
|
+
let kind = null;
|
|
1564
|
+
for (const raw of rawSeries) {
|
|
1565
|
+
if (!isPlainObject(raw)) continue;
|
|
1566
|
+
const type = typeof raw.type === "string" ? raw.type.toLowerCase() : "bar";
|
|
1567
|
+
if (!SUPPORTED_TYPES.includes(type)) return null;
|
|
1568
|
+
const normalised = normaliseSeries(raw, type);
|
|
1569
|
+
if (normalised === null) return null;
|
|
1570
|
+
kind ?? (kind = type);
|
|
1571
|
+
series.push(normalised);
|
|
1572
|
+
if (type === "pie" && categories.length === 0) {
|
|
1573
|
+
categories = pieCategories(raw);
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
if (series.length === 0 || kind === null) return null;
|
|
1577
|
+
return { kind, title: extractTitle(option), categories, series };
|
|
1578
|
+
}
|
|
1579
|
+
};
|
|
1580
|
+
function extractSeries(option) {
|
|
1581
|
+
const series = option?.series ?? null;
|
|
1582
|
+
if (Array.isArray(series)) return series;
|
|
1583
|
+
if (isPlainObject(series) && Object.keys(series).length > 0) return [series];
|
|
1584
|
+
return [];
|
|
1585
|
+
}
|
|
1586
|
+
function extractCategories(option) {
|
|
1587
|
+
let candidates = null;
|
|
1588
|
+
const xAxis = option?.xAxis ?? null;
|
|
1589
|
+
if (Array.isArray(xAxis)) {
|
|
1590
|
+
candidates = isPlainObject(xAxis[0]) ? xAxis[0].data : xAxis.data;
|
|
1591
|
+
} else if (isPlainObject(xAxis)) {
|
|
1592
|
+
candidates = xAxis.data;
|
|
1593
|
+
}
|
|
1594
|
+
if (!Array.isArray(candidates)) candidates = option?.categories ?? null;
|
|
1595
|
+
if (!Array.isArray(candidates)) return [];
|
|
1596
|
+
const out = [];
|
|
1597
|
+
for (const value of candidates) {
|
|
1598
|
+
if (isScalar(value)) out.push(String(value));
|
|
1599
|
+
}
|
|
1600
|
+
return out;
|
|
1601
|
+
}
|
|
1602
|
+
function extractTitle(option) {
|
|
1603
|
+
const title = option?.title ?? null;
|
|
1604
|
+
if (isPlainObject(title)) {
|
|
1605
|
+
const text = title.text ?? title[0]?.text ?? null;
|
|
1606
|
+
if (typeof text === "string") return text;
|
|
1607
|
+
}
|
|
1608
|
+
if (Array.isArray(title)) {
|
|
1609
|
+
const text = title[0]?.text ?? null;
|
|
1610
|
+
if (typeof text === "string") return text;
|
|
1611
|
+
}
|
|
1612
|
+
if (typeof title === "string") return title;
|
|
1613
|
+
return "";
|
|
1614
|
+
}
|
|
1615
|
+
function normaliseSeries(raw, type) {
|
|
1616
|
+
const name = isScalar(raw.name) ? String(raw.name) : "";
|
|
1617
|
+
const data = raw.data ?? null;
|
|
1618
|
+
if (!Array.isArray(data)) return null;
|
|
1619
|
+
const values = [];
|
|
1620
|
+
const points = [];
|
|
1621
|
+
for (const point of data) {
|
|
1622
|
+
if (type === "scatter") {
|
|
1623
|
+
const xy = scatterPoint(point);
|
|
1624
|
+
if (xy !== null) points.push(xy);
|
|
1625
|
+
continue;
|
|
1626
|
+
}
|
|
1627
|
+
values.push(numericValue(point));
|
|
1628
|
+
}
|
|
1629
|
+
if (type === "scatter" && points.length === 0) return null;
|
|
1630
|
+
if (type !== "scatter" && values.length === 0) return null;
|
|
1631
|
+
const area = type === "line" && raw.areaStyle !== void 0;
|
|
1632
|
+
return { type, name, values, smooth: !!raw.smooth, area, points };
|
|
1633
|
+
}
|
|
1634
|
+
function numericValue(point) {
|
|
1635
|
+
if (isNumeric(point)) return Number(point);
|
|
1636
|
+
if (isPlainObject(point) && isNumeric(point.value)) return Number(point.value);
|
|
1637
|
+
return 0;
|
|
1638
|
+
}
|
|
1639
|
+
function scatterPoint(point) {
|
|
1640
|
+
let pair = point;
|
|
1641
|
+
if (isPlainObject(point) && Array.isArray(point.value)) pair = point.value;
|
|
1642
|
+
if (Array.isArray(pair) && pair.length >= 2 && isNumeric(pair[0]) && isNumeric(pair[1])) {
|
|
1643
|
+
return { x: Number(pair[0]), y: Number(pair[1]) };
|
|
1644
|
+
}
|
|
1645
|
+
return null;
|
|
1646
|
+
}
|
|
1647
|
+
function pieCategories(raw) {
|
|
1648
|
+
const data = raw.data ?? null;
|
|
1649
|
+
if (!Array.isArray(data)) return [];
|
|
1650
|
+
return data.map(
|
|
1651
|
+
(point, i) => isPlainObject(point) && isScalar(point.name) ? String(point.name) : `Slice ${i + 1}`
|
|
1652
|
+
);
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1655
|
+
// src/helpers/color.ts
|
|
1656
|
+
var NAMED = {
|
|
1657
|
+
black: "000000",
|
|
1658
|
+
white: "FFFFFF",
|
|
1659
|
+
red: "FF0000",
|
|
1660
|
+
green: "008000",
|
|
1661
|
+
blue: "0000FF",
|
|
1662
|
+
yellow: "FFFF00",
|
|
1663
|
+
cyan: "00FFFF",
|
|
1664
|
+
magenta: "FF00FF",
|
|
1665
|
+
gray: "808080",
|
|
1666
|
+
grey: "808080",
|
|
1667
|
+
silver: "C0C0C0",
|
|
1668
|
+
maroon: "800000",
|
|
1669
|
+
olive: "808000",
|
|
1670
|
+
lime: "00FF00",
|
|
1671
|
+
aqua: "00FFFF",
|
|
1672
|
+
teal: "008080",
|
|
1673
|
+
navy: "000080",
|
|
1674
|
+
fuchsia: "FF00FF",
|
|
1675
|
+
purple: "800080",
|
|
1676
|
+
orange: "FFA500"
|
|
1677
|
+
};
|
|
1678
|
+
var Color = {
|
|
1679
|
+
parse(color, fallbackHex = "000000") {
|
|
1680
|
+
if (color === null || color === void 0 || color === "") return [fallbackHex, 1e5];
|
|
1681
|
+
const c = color.trim();
|
|
1682
|
+
if (c === "transparent" || c === "none") return [fallbackHex, 0];
|
|
1683
|
+
let m = /^#([0-9a-fA-F]{3})$/.exec(c);
|
|
1684
|
+
if (m) {
|
|
1685
|
+
const h = m[1];
|
|
1686
|
+
const hex = (h[0] + h[0] + h[1] + h[1] + h[2] + h[2]).toUpperCase();
|
|
1687
|
+
return [hex, 1e5];
|
|
1688
|
+
}
|
|
1689
|
+
m = /^#([0-9a-fA-F]{6})$/.exec(c);
|
|
1690
|
+
if (m) return [m[1].toUpperCase(), 1e5];
|
|
1691
|
+
m = /^#([0-9a-fA-F]{8})$/.exec(c);
|
|
1692
|
+
if (m) {
|
|
1693
|
+
const hex = m[1].slice(0, 6).toUpperCase();
|
|
1694
|
+
const a = parseInt(m[1].slice(6, 8), 16);
|
|
1695
|
+
return [hex, Math.round(a / 255 * 1e5)];
|
|
1696
|
+
}
|
|
1697
|
+
m = /^rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*(?:,\s*([0-9.]+)\s*)?\)$/i.exec(c);
|
|
1698
|
+
if (m) {
|
|
1699
|
+
const r = parseInt(m[1], 10);
|
|
1700
|
+
const g = parseInt(m[2], 10);
|
|
1701
|
+
const b = parseInt(m[3], 10);
|
|
1702
|
+
const a = m[4] !== void 0 ? parseFloat(m[4]) : 1;
|
|
1703
|
+
const hex = [r, g, b].map((n) => n.toString(16).padStart(2, "0")).join("").toUpperCase();
|
|
1704
|
+
return [hex, Math.round(a * 1e5)];
|
|
1705
|
+
}
|
|
1706
|
+
const named = NAMED[c.toLowerCase()];
|
|
1707
|
+
if (named !== void 0) return [named, 1e5];
|
|
1708
|
+
return [fallbackHex, 1e5];
|
|
1709
|
+
}
|
|
1710
|
+
};
|
|
1711
|
+
|
|
1712
|
+
// src/helpers/markdown-inline.ts
|
|
1713
|
+
var isWordChar = (ch) => /[a-zA-Z0-9_]/.test(ch);
|
|
1714
|
+
var MarkdownInline = {
|
|
1715
|
+
tokenize(text) {
|
|
1716
|
+
const runs = [];
|
|
1717
|
+
let i = 0;
|
|
1718
|
+
const len = text.length;
|
|
1719
|
+
let buf = "";
|
|
1720
|
+
let b = false;
|
|
1721
|
+
let it = false;
|
|
1722
|
+
const code = false;
|
|
1723
|
+
const flush = () => {
|
|
1724
|
+
if (buf !== "") {
|
|
1725
|
+
runs.push({ text: buf, b, i: it, code });
|
|
1726
|
+
buf = "";
|
|
1727
|
+
}
|
|
1728
|
+
};
|
|
1729
|
+
while (i < len) {
|
|
1730
|
+
const c = text[i];
|
|
1731
|
+
const next2 = text.slice(i, i + 2);
|
|
1732
|
+
if (c === "`" && !code) {
|
|
1733
|
+
flush();
|
|
1734
|
+
const end = text.indexOf("`", i + 1);
|
|
1735
|
+
if (end === -1) {
|
|
1736
|
+
buf += text.slice(i);
|
|
1737
|
+
i = len;
|
|
1738
|
+
continue;
|
|
1739
|
+
}
|
|
1740
|
+
runs.push({ text: text.slice(i + 1, end), b, i: it, code: true });
|
|
1741
|
+
i = end + 1;
|
|
1742
|
+
continue;
|
|
1743
|
+
}
|
|
1744
|
+
if (next2 === "**" || next2 === "__") {
|
|
1745
|
+
flush();
|
|
1746
|
+
b = !b;
|
|
1747
|
+
i += 2;
|
|
1748
|
+
continue;
|
|
1749
|
+
}
|
|
1750
|
+
if ((c === "*" || c === "_") && !code) {
|
|
1751
|
+
const prev = i > 0 ? text[i - 1] : " ";
|
|
1752
|
+
if (it && isWordChar(prev) || !it) {
|
|
1753
|
+
if (!it) {
|
|
1754
|
+
if (!isWordChar(prev) || prev === " ") {
|
|
1755
|
+
flush();
|
|
1756
|
+
it = true;
|
|
1757
|
+
i++;
|
|
1758
|
+
continue;
|
|
1759
|
+
}
|
|
1760
|
+
} else {
|
|
1761
|
+
flush();
|
|
1762
|
+
it = false;
|
|
1763
|
+
i++;
|
|
1764
|
+
continue;
|
|
1765
|
+
}
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
buf += c;
|
|
1769
|
+
i++;
|
|
1770
|
+
}
|
|
1771
|
+
flush();
|
|
1772
|
+
if (runs.length === 0) {
|
|
1773
|
+
runs.push({ text: "", b: false, i: false, code: false });
|
|
1774
|
+
}
|
|
1775
|
+
return runs;
|
|
1776
|
+
},
|
|
1777
|
+
/** [isBullet, contentWithoutMarker]. */
|
|
1778
|
+
bulletPrefix(line) {
|
|
1779
|
+
if (line.startsWith("- ") || line.startsWith("* ")) return [true, line.slice(2)];
|
|
1780
|
+
return [false, line];
|
|
1781
|
+
},
|
|
1782
|
+
/** [level (1..6, 0=none), contentWithoutMarker]. */
|
|
1783
|
+
headingPrefix(line) {
|
|
1784
|
+
const m = /^(#{1,6})\s+(.*)$/.exec(line);
|
|
1785
|
+
if (m) return [m[1].length, m[2]];
|
|
1786
|
+
return [0, line];
|
|
1787
|
+
}
|
|
1788
|
+
};
|
|
1789
|
+
|
|
1790
|
+
// src/helpers/syntax-highlighter.ts
|
|
1791
|
+
var S = (re) => new RegExp(re.source, "sy");
|
|
1792
|
+
var C_LIKE = {
|
|
1793
|
+
comment: S(/\/\/[^\n]*|\/\*.*?\*\//),
|
|
1794
|
+
string: S(/"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|`(?:[^`\\]|\\.)*`/),
|
|
1795
|
+
number: S(/\b\d+(?:\.\d+)?\b/),
|
|
1796
|
+
word: S(/[A-Za-z_$][A-Za-z0-9_$]*/),
|
|
1797
|
+
punctuation: S(/[{}\[\]();,]/)
|
|
1798
|
+
};
|
|
1799
|
+
function config(lang) {
|
|
1800
|
+
if (lang === null) return null;
|
|
1801
|
+
switch (lang) {
|
|
1802
|
+
case "javascript":
|
|
1803
|
+
case "typescript":
|
|
1804
|
+
return {
|
|
1805
|
+
patterns: C_LIKE,
|
|
1806
|
+
keywords: ["const", "let", "var", "function", "return", "if", "else", "for", "while", "do", "switch", "case", "break", "continue", "new", "class", "extends", "implements", "interface", "type", "enum", "import", "export", "from", "as", "default", "async", "await", "try", "catch", "finally", "throw", "typeof", "instanceof", "in", "of", "this", "super", "null", "undefined", "true", "false", "void", "never", "any", "unknown", "string", "number", "boolean", "object"],
|
|
1807
|
+
builtins: ["console", "Math", "JSON", "Object", "Array", "String", "Number", "Boolean", "Promise", "Map", "Set", "Date", "Error", "document", "window", "globalThis", "require", "module", "process"]
|
|
1808
|
+
};
|
|
1809
|
+
case "php":
|
|
1810
|
+
return {
|
|
1811
|
+
patterns: {
|
|
1812
|
+
comment: S(/\/\/[^\n]*|#[^\n]*|\/\*.*?\*\//),
|
|
1813
|
+
string: S(/"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'/),
|
|
1814
|
+
number: S(/\b\d+(?:\.\d+)?\b/),
|
|
1815
|
+
word: S(/\$?[A-Za-z_][A-Za-z0-9_]*/),
|
|
1816
|
+
punctuation: S(/[{}\[\]();,]/)
|
|
1817
|
+
},
|
|
1818
|
+
keywords: ["abstract", "and", "array", "as", "break", "callable", "case", "catch", "class", "clone", "const", "continue", "declare", "default", "do", "echo", "else", "elseif", "empty", "enddeclare", "endfor", "endforeach", "endif", "endswitch", "endwhile", "enum", "extends", "final", "finally", "fn", "for", "foreach", "function", "global", "goto", "if", "implements", "include", "include_once", "instanceof", "insteadof", "interface", "isset", "list", "match", "namespace", "new", "null", "or", "private", "protected", "public", "readonly", "require", "require_once", "return", "static", "switch", "throw", "trait", "try", "unset", "use", "var", "while", "xor", "yield", "true", "false", "self", "parent", "this"],
|
|
1819
|
+
builtins: ["__construct", "__toString", "__invoke", "array_map", "array_filter", "array_reduce", "array_keys", "array_values", "array_merge", "count", "strlen", "str_replace", "str_starts_with", "str_ends_with", "str_contains", "substr", "sprintf", "printf", "json_encode", "json_decode"]
|
|
1820
|
+
};
|
|
1821
|
+
case "json":
|
|
1822
|
+
return {
|
|
1823
|
+
patterns: {
|
|
1824
|
+
string: S(/"(?:[^"\\]|\\.)*"/),
|
|
1825
|
+
number: S(/-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?/),
|
|
1826
|
+
word: S(/[A-Za-z_][A-Za-z0-9_]*/),
|
|
1827
|
+
punctuation: S(/[{}\[\]:,]/)
|
|
1828
|
+
},
|
|
1829
|
+
keywords: ["true", "false", "null"],
|
|
1830
|
+
builtins: []
|
|
1831
|
+
};
|
|
1832
|
+
case "bash":
|
|
1833
|
+
return {
|
|
1834
|
+
patterns: {
|
|
1835
|
+
comment: S(/#[^\n]*/),
|
|
1836
|
+
string: S(/"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'/),
|
|
1837
|
+
number: S(/\b\d+\b/),
|
|
1838
|
+
word: S(/[A-Za-z_][A-Za-z0-9_]*/),
|
|
1839
|
+
punctuation: S(/[{}\[\]();|&]/)
|
|
1840
|
+
},
|
|
1841
|
+
keywords: ["if", "then", "else", "elif", "fi", "for", "while", "do", "done", "case", "esac", "in", "function", "return", "break", "continue", "export", "local", "readonly", "unset"],
|
|
1842
|
+
builtins: ["echo", "printf", "cd", "pwd", "ls", "cp", "mv", "rm", "mkdir", "touch", "cat", "grep", "sed", "awk", "curl", "wget", "git", "npm", "composer", "php", "node"]
|
|
1843
|
+
};
|
|
1844
|
+
case "css":
|
|
1845
|
+
return {
|
|
1846
|
+
patterns: {
|
|
1847
|
+
comment: S(/\/\*.*?\*\//),
|
|
1848
|
+
string: S(/"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'/),
|
|
1849
|
+
number: S(/-?\d+(?:\.\d+)?(?:px|em|rem|%|vw|vh|deg)?/),
|
|
1850
|
+
word: S(/[\-A-Za-z_][\-A-Za-z0-9_]*/),
|
|
1851
|
+
punctuation: S(/[{};:,]/)
|
|
1852
|
+
},
|
|
1853
|
+
keywords: ["important", "inherit", "initial", "unset", "auto", "none"],
|
|
1854
|
+
builtins: []
|
|
1855
|
+
};
|
|
1856
|
+
case "python":
|
|
1857
|
+
return {
|
|
1858
|
+
patterns: {
|
|
1859
|
+
comment: S(/#[^\n]*/),
|
|
1860
|
+
string: S(/"""[\s\S]*?"""|'''[\s\S]*?'''|"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'/),
|
|
1861
|
+
number: S(/\b\d+(?:\.\d+)?\b/),
|
|
1862
|
+
word: S(/[A-Za-z_][A-Za-z0-9_]*/),
|
|
1863
|
+
punctuation: S(/[{}\[\]():,]/)
|
|
1864
|
+
},
|
|
1865
|
+
keywords: ["and", "as", "assert", "async", "await", "break", "class", "continue", "def", "del", "elif", "else", "except", "finally", "for", "from", "global", "if", "import", "in", "is", "lambda", "nonlocal", "not", "or", "pass", "raise", "return", "try", "while", "with", "yield", "True", "False", "None"],
|
|
1866
|
+
builtins: ["print", "len", "range", "list", "dict", "set", "tuple", "str", "int", "float", "bool", "open", "input", "enumerate", "zip", "map", "filter", "sorted", "reversed", "sum", "min", "max", "abs", "round"]
|
|
1867
|
+
};
|
|
1868
|
+
case "html":
|
|
1869
|
+
return {
|
|
1870
|
+
patterns: {
|
|
1871
|
+
comment: S(/<!--.*?-->/),
|
|
1872
|
+
string: S(/"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'/),
|
|
1873
|
+
keyword: S(/<\/?[A-Za-z][A-Za-z0-9-]*/),
|
|
1874
|
+
punctuation: S(/>|\/>|=/)
|
|
1875
|
+
},
|
|
1876
|
+
keywords: [],
|
|
1877
|
+
builtins: []
|
|
1878
|
+
};
|
|
1879
|
+
default:
|
|
1880
|
+
return null;
|
|
1881
|
+
}
|
|
1882
|
+
}
|
|
1883
|
+
function normalizeLanguage(language) {
|
|
1884
|
+
if (language === null || language === void 0) return null;
|
|
1885
|
+
const lang = language.toLowerCase().trim();
|
|
1886
|
+
switch (lang) {
|
|
1887
|
+
case "js":
|
|
1888
|
+
return "javascript";
|
|
1889
|
+
case "ts":
|
|
1890
|
+
return "typescript";
|
|
1891
|
+
case "jsx":
|
|
1892
|
+
case "tsx":
|
|
1893
|
+
return "typescript";
|
|
1894
|
+
case "sh":
|
|
1895
|
+
case "shell":
|
|
1896
|
+
case "bash":
|
|
1897
|
+
case "zsh":
|
|
1898
|
+
return "bash";
|
|
1899
|
+
case "py":
|
|
1900
|
+
return "python";
|
|
1901
|
+
case "xml":
|
|
1902
|
+
return "html";
|
|
1903
|
+
default:
|
|
1904
|
+
return lang;
|
|
1905
|
+
}
|
|
1906
|
+
}
|
|
1907
|
+
var SyntaxHighlighter = {
|
|
1908
|
+
tokenize(code, language) {
|
|
1909
|
+
const lang = normalizeLanguage(language);
|
|
1910
|
+
const cfg = config(lang);
|
|
1911
|
+
if (cfg === null) return [{ text: code, kind: "plain" }];
|
|
1912
|
+
const tokens = [];
|
|
1913
|
+
let offset = 0;
|
|
1914
|
+
const len = code.length;
|
|
1915
|
+
const keywords = new Set(cfg.keywords);
|
|
1916
|
+
const builtins = new Set(cfg.builtins);
|
|
1917
|
+
while (offset < len) {
|
|
1918
|
+
let bestKind = null;
|
|
1919
|
+
let bestLen = 0;
|
|
1920
|
+
for (const [kind2, re] of Object.entries(cfg.patterns)) {
|
|
1921
|
+
re.lastIndex = offset;
|
|
1922
|
+
const m = re.exec(code);
|
|
1923
|
+
if (m && m.index === offset) {
|
|
1924
|
+
const matchLen = m[0].length;
|
|
1925
|
+
if (matchLen > bestLen) {
|
|
1926
|
+
bestLen = matchLen;
|
|
1927
|
+
bestKind = kind2;
|
|
1928
|
+
}
|
|
1929
|
+
}
|
|
1930
|
+
}
|
|
1931
|
+
if (bestKind === null || bestLen === 0) {
|
|
1932
|
+
tokens.push({ text: code[offset], kind: "plain" });
|
|
1933
|
+
offset++;
|
|
1934
|
+
continue;
|
|
1935
|
+
}
|
|
1936
|
+
const text = code.slice(offset, offset + bestLen);
|
|
1937
|
+
let kind = bestKind;
|
|
1938
|
+
if (kind === "word") {
|
|
1939
|
+
if (keywords.has(text)) kind = "keyword";
|
|
1940
|
+
else if (builtins.has(text)) kind = "builtin";
|
|
1941
|
+
else kind = "plain";
|
|
1942
|
+
}
|
|
1943
|
+
tokens.push({ text, kind });
|
|
1944
|
+
offset += bestLen;
|
|
1945
|
+
}
|
|
1946
|
+
return coalesce(tokens);
|
|
1947
|
+
},
|
|
1948
|
+
colorFor(kind) {
|
|
1949
|
+
switch (kind) {
|
|
1950
|
+
case "keyword":
|
|
1951
|
+
return "C084FC";
|
|
1952
|
+
case "string":
|
|
1953
|
+
return "86EFAC";
|
|
1954
|
+
case "comment":
|
|
1955
|
+
return "64748B";
|
|
1956
|
+
case "number":
|
|
1957
|
+
return "FBBF24";
|
|
1958
|
+
case "builtin":
|
|
1959
|
+
return "67E8F9";
|
|
1960
|
+
case "punctuation":
|
|
1961
|
+
return "CBD5E1";
|
|
1962
|
+
default:
|
|
1963
|
+
return "F8FAFC";
|
|
1964
|
+
}
|
|
1965
|
+
},
|
|
1966
|
+
supportedLanguages() {
|
|
1967
|
+
return ["javascript", "typescript", "jsx", "tsx", "php", "json", "bash", "shell", "css", "python", "html", "xml"];
|
|
1968
|
+
}
|
|
1969
|
+
};
|
|
1970
|
+
function coalesce(tokens) {
|
|
1971
|
+
const out = [];
|
|
1972
|
+
for (const t of tokens) {
|
|
1973
|
+
const last = out[out.length - 1];
|
|
1974
|
+
if (last && last.kind === t.kind) {
|
|
1975
|
+
last.text += t.text;
|
|
1976
|
+
continue;
|
|
1977
|
+
}
|
|
1978
|
+
out.push({ ...t });
|
|
1979
|
+
}
|
|
1980
|
+
return out;
|
|
1981
|
+
}
|
|
1982
|
+
|
|
1983
|
+
// src/helpers/xml.ts
|
|
1984
|
+
var Xml = {
|
|
1985
|
+
text(s) {
|
|
1986
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
1987
|
+
},
|
|
1988
|
+
attr(s) {
|
|
1989
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
1990
|
+
},
|
|
1991
|
+
declaration(standalone = true) {
|
|
1992
|
+
const sa = standalone ? ' standalone="yes"' : "";
|
|
1993
|
+
return `<?xml version="1.0" encoding="UTF-8"${sa}?>
|
|
1994
|
+
`;
|
|
1995
|
+
}
|
|
1996
|
+
};
|
|
1997
|
+
|
|
1998
|
+
// src/writer/pptx-writer.ts
|
|
1999
|
+
var NS_CHART = "http://schemas.openxmlformats.org/drawingml/2006/chart";
|
|
2000
|
+
var LAYOUT_ORDER = [
|
|
2001
|
+
"blank",
|
|
2002
|
+
"title",
|
|
2003
|
+
"title-content",
|
|
2004
|
+
"two-column",
|
|
2005
|
+
"section-divider",
|
|
2006
|
+
"image-text",
|
|
2007
|
+
"text-image",
|
|
2008
|
+
"quote"
|
|
2009
|
+
];
|
|
2010
|
+
var CHART_PALETTE = ["8B5CF6", "EC4899", "06B6D4", "F59E0B", "10B981", "3B82F6", "EF4444", "A855F7"];
|
|
2011
|
+
var ENCODER = new TextEncoder();
|
|
2012
|
+
function base64Decode(b64) {
|
|
2013
|
+
if (typeof Buffer !== "undefined") {
|
|
2014
|
+
const buf = Buffer.from(b64, "base64");
|
|
2015
|
+
return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
|
|
2016
|
+
}
|
|
2017
|
+
const bin = atob(b64);
|
|
2018
|
+
const out = new Uint8Array(bin.length);
|
|
2019
|
+
for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
|
|
2020
|
+
return out;
|
|
2021
|
+
}
|
|
2022
|
+
function toInt(v) {
|
|
2023
|
+
const n = typeof v === "number" ? v : parseFloat(String(v));
|
|
2024
|
+
if (!Number.isFinite(n)) return 0;
|
|
2025
|
+
return Math.trunc(n);
|
|
2026
|
+
}
|
|
2027
|
+
var PptxWriter = class {
|
|
2028
|
+
constructor(tempDir = null, allowHttpImages = false) {
|
|
2029
|
+
this.tempDir = tempDir;
|
|
2030
|
+
this.allowHttpImages = allowHttpImages;
|
|
2031
|
+
this.mediaCounter = 0;
|
|
2032
|
+
this.chartCounter = 0;
|
|
2033
|
+
/** Ordered list of media files queued for the archive. */
|
|
2034
|
+
this.mediaFiles = [];
|
|
2035
|
+
/** Ordered list of chart part XML queued for the archive. */
|
|
2036
|
+
this.chartFiles = [];
|
|
2037
|
+
this.themeAccent = "8B5CF6";
|
|
2038
|
+
this.tnId = 0;
|
|
2039
|
+
this.pendingSlideRels = {};
|
|
2040
|
+
}
|
|
2041
|
+
write() {
|
|
2042
|
+
throw new Error("PptxWriter.write is Node-only; use Agent.write");
|
|
2043
|
+
}
|
|
2044
|
+
toBytes(deck) {
|
|
2045
|
+
this.mediaCounter = 0;
|
|
2046
|
+
this.chartCounter = 0;
|
|
2047
|
+
this.mediaFiles = [];
|
|
2048
|
+
this.chartFiles = [];
|
|
2049
|
+
this.pendingSlideRels = {};
|
|
2050
|
+
[this.themeAccent] = Color.parse(deck?.theme?.colors?.accent ?? "#8B5CF6", "8B5CF6");
|
|
2051
|
+
const slides = deck?.slides ?? [];
|
|
2052
|
+
const slideCount = slides.length;
|
|
2053
|
+
const files = [];
|
|
2054
|
+
const add = (name, content) => {
|
|
2055
|
+
files.push({ name, data: typeof content === "string" ? ENCODER.encode(content) : content });
|
|
2056
|
+
};
|
|
2057
|
+
const slidesXml = {};
|
|
2058
|
+
const notesSlidesXml = {};
|
|
2059
|
+
slides.forEach((slide, i) => {
|
|
2060
|
+
const oneBased = i + 1;
|
|
2061
|
+
slidesXml[oneBased] = this.buildSlideXml(slide, oneBased, deck);
|
|
2062
|
+
if (slide && slide.notes) {
|
|
2063
|
+
notesSlidesXml[oneBased] = this.buildNotesSlideXml(slide, oneBased);
|
|
2064
|
+
}
|
|
2065
|
+
});
|
|
2066
|
+
const notesIds = Object.keys(notesSlidesXml).map((k) => parseInt(k, 10));
|
|
2067
|
+
const chartPartPaths = this.chartFiles.map((c) => c.path);
|
|
2068
|
+
add("[Content_Types].xml", this.buildContentTypes(slideCount, notesIds, chartPartPaths));
|
|
2069
|
+
add("_rels/.rels", this.buildTopRels());
|
|
2070
|
+
add("docProps/core.xml", this.buildCoreProps(deck));
|
|
2071
|
+
add("docProps/app.xml", this.buildAppProps(slideCount));
|
|
2072
|
+
add("ppt/presentation.xml", this.buildPresentation(slideCount));
|
|
2073
|
+
add("ppt/_rels/presentation.xml.rels", this.buildPresentationRels(slideCount));
|
|
2074
|
+
add("ppt/theme/theme1.xml", this.buildTheme(deck));
|
|
2075
|
+
add("ppt/slideMasters/slideMaster1.xml", this.buildSlideMaster());
|
|
2076
|
+
add("ppt/slideMasters/_rels/slideMaster1.xml.rels", this.buildSlideMasterRels());
|
|
2077
|
+
LAYOUT_ORDER.forEach((layoutName, idx) => {
|
|
2078
|
+
const n = idx + 1;
|
|
2079
|
+
add(`ppt/slideLayouts/slideLayout${n}.xml`, this.buildSlideLayout(layoutName));
|
|
2080
|
+
add(`ppt/slideLayouts/_rels/slideLayout${n}.xml.rels`, this.buildSlideLayoutRels());
|
|
2081
|
+
});
|
|
2082
|
+
for (let i = 1; i <= slideCount; i++) {
|
|
2083
|
+
const xml = slidesXml[i];
|
|
2084
|
+
const layoutNum = this.layoutNumberFor(slides[i - 1]?.layout ?? null);
|
|
2085
|
+
add(`ppt/slides/slide${i}.xml`, xml);
|
|
2086
|
+
add(`ppt/slides/_rels/slide${i}.xml.rels`, this.buildSlideRels(i, notesSlidesXml[i] !== void 0, layoutNum));
|
|
2087
|
+
}
|
|
2088
|
+
for (const i of notesIds) {
|
|
2089
|
+
add(`ppt/notesSlides/notesSlide${i}.xml`, notesSlidesXml[i]);
|
|
2090
|
+
add(`ppt/notesSlides/_rels/notesSlide${i}.xml.rels`, this.buildNotesSlideRels(i));
|
|
2091
|
+
}
|
|
2092
|
+
for (const chart of this.chartFiles) {
|
|
2093
|
+
add(chart.path, chart.xml);
|
|
2094
|
+
}
|
|
2095
|
+
for (const media of this.mediaFiles) {
|
|
2096
|
+
add(media.path, media.bytes);
|
|
2097
|
+
}
|
|
2098
|
+
return zipSync(files);
|
|
2099
|
+
}
|
|
2100
|
+
// ─── Top-level parts ───────────────────────────────────────────────────
|
|
2101
|
+
buildContentTypes(slideCount, notesSlideIds, chartParts) {
|
|
2102
|
+
let slideOverrides = "";
|
|
2103
|
+
for (let i = 1; i <= slideCount; i++) {
|
|
2104
|
+
slideOverrides += '<Override PartName="/ppt/slides/slide' + i + '.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.slide+xml"/>';
|
|
2105
|
+
}
|
|
2106
|
+
let layoutOverrides = "";
|
|
2107
|
+
LAYOUT_ORDER.forEach((_, idx) => {
|
|
2108
|
+
const n = idx + 1;
|
|
2109
|
+
layoutOverrides += '<Override PartName="/ppt/slideLayouts/slideLayout' + n + '.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.slideLayout+xml"/>';
|
|
2110
|
+
});
|
|
2111
|
+
let notesOverrides = "";
|
|
2112
|
+
for (const i of notesSlideIds) {
|
|
2113
|
+
notesOverrides += '<Override PartName="/ppt/notesSlides/notesSlide' + i + '.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.notesSlide+xml"/>';
|
|
2114
|
+
}
|
|
2115
|
+
let chartOverrides = "";
|
|
2116
|
+
for (const archivePath of chartParts) {
|
|
2117
|
+
chartOverrides += '<Override PartName="/' + archivePath + '" ContentType="application/vnd.openxmlformats-officedocument.drawingml.chart+xml"/>';
|
|
2118
|
+
}
|
|
2119
|
+
const extensionDefaults = '<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Default Extension="xml" ContentType="application/xml"/><Default Extension="png" ContentType="image/png"/><Default Extension="jpg" ContentType="image/jpeg"/><Default Extension="jpeg" ContentType="image/jpeg"/><Default Extension="gif" ContentType="image/gif"/><Default Extension="svg" ContentType="image/svg+xml"/><Default Extension="webp" ContentType="image/webp"/>';
|
|
2120
|
+
return Xml.declaration() + '<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">' + extensionDefaults + '<Override PartName="/ppt/presentation.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.presentation.main+xml"/><Override PartName="/ppt/slideMasters/slideMaster1.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.slideMaster+xml"/>' + layoutOverrides + '<Override PartName="/ppt/theme/theme1.xml" ContentType="application/vnd.openxmlformats-officedocument.theme+xml"/>' + slideOverrides + notesOverrides + chartOverrides + '<Override PartName="/docProps/core.xml" ContentType="application/vnd.openxmlformats-package.core-properties+xml"/><Override PartName="/docProps/app.xml" ContentType="application/vnd.openxmlformats-officedocument.extended-properties+xml"/></Types>';
|
|
2121
|
+
}
|
|
2122
|
+
buildTopRels() {
|
|
2123
|
+
return Xml.declaration() + '<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="ppt/presentation.xml"/><Relationship Id="rId2" Type="http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties" Target="docProps/core.xml"/><Relationship Id="rId3" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties" Target="docProps/app.xml"/></Relationships>';
|
|
2124
|
+
}
|
|
2125
|
+
buildCoreProps(deck) {
|
|
2126
|
+
const title = Xml.text(String(deck?.title ?? "Untitled"));
|
|
2127
|
+
const author = deck?.metadata?.author !== void 0 && deck?.metadata?.author !== null ? Xml.text(String(deck.metadata.author)) : "Dark Slide";
|
|
2128
|
+
const now = (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
2129
|
+
return Xml.declaration() + `<cp:coreProperties xmlns:cp="http://schemas.openxmlformats.org/package/2006/metadata/core-properties" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:dcterms="http://purl.org/dc/terms/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"><dc:title>${title}</dc:title><dc:creator>${author}</dc:creator><cp:lastModifiedBy>${author}</cp:lastModifiedBy><dcterms:created xsi:type="dcterms:W3CDTF">${now}</dcterms:created><dcterms:modified xsi:type="dcterms:W3CDTF">${now}</dcterms:modified></cp:coreProperties>`;
|
|
2130
|
+
}
|
|
2131
|
+
buildAppProps(slideCount) {
|
|
2132
|
+
return Xml.declaration() + `<Properties xmlns="http://schemas.openxmlformats.org/officeDocument/2006/extended-properties" xmlns:vt="http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes"><Application>DarkSlide</Application><AppVersion>0.4.0</AppVersion><Slides>${slideCount}</Slides></Properties>`;
|
|
2133
|
+
}
|
|
2134
|
+
// ─── presentation.xml ──────────────────────────────────────────────────
|
|
2135
|
+
buildPresentation(slideCount) {
|
|
2136
|
+
let sldIdLst = "";
|
|
2137
|
+
for (let i = 1; i <= slideCount; i++) {
|
|
2138
|
+
const id = 256 + (i - 1);
|
|
2139
|
+
sldIdLst += '<p:sldId id="' + id + '" r:id="rId' + (i + 1) + '"/>';
|
|
2140
|
+
}
|
|
2141
|
+
const slideMasterRid = "rId" + (slideCount + 2);
|
|
2142
|
+
return Xml.declaration() + '<p:presentation xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main" saveSubsetFonts="1"><p:sldMasterIdLst><p:sldMasterId id="2147483648" r:id="' + slideMasterRid + '"/></p:sldMasterIdLst><p:sldIdLst>' + sldIdLst + '</p:sldIdLst><p:sldSz cx="' + Emu.DEFAULT_SLIDE_WIDTH + '" cy="' + Emu.DEFAULT_SLIDE_HEIGHT + '" type="screen16x9"/><p:notesSz cx="' + Emu.DEFAULT_SLIDE_HEIGHT + '" cy="' + Emu.DEFAULT_SLIDE_WIDTH + '"/></p:presentation>';
|
|
2143
|
+
}
|
|
2144
|
+
buildPresentationRels(slideCount) {
|
|
2145
|
+
let rels = '<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme" Target="theme/theme1.xml"/>';
|
|
2146
|
+
for (let i = 1; i <= slideCount; i++) {
|
|
2147
|
+
rels += '<Relationship Id="rId' + (i + 1) + '" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide" Target="slides/slide' + i + '.xml"/>';
|
|
2148
|
+
}
|
|
2149
|
+
rels += '<Relationship Id="rId' + (slideCount + 2) + '" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideMaster" Target="slideMasters/slideMaster1.xml"/>';
|
|
2150
|
+
return Xml.declaration() + '<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">' + rels + "</Relationships>";
|
|
2151
|
+
}
|
|
2152
|
+
// ─── theme / master / layout ───────────────────────────────────────────
|
|
2153
|
+
buildTheme(deck) {
|
|
2154
|
+
const colors = deck?.theme?.colors ?? {};
|
|
2155
|
+
const [bg] = Color.parse(colors.background ?? "#FFFFFF", "FFFFFF");
|
|
2156
|
+
const [text] = Color.parse(colors.text ?? "#0F172A", "0F172A");
|
|
2157
|
+
const [accent] = Color.parse(colors.accent ?? "#8B5CF6", "8B5CF6");
|
|
2158
|
+
const [muted] = Color.parse(colors.muted ?? "#44546A", "44546A");
|
|
2159
|
+
const [surface] = Color.parse(colors.surface ?? "#E7E6E6", "E7E6E6");
|
|
2160
|
+
const heading = Xml.attr(String(deck?.theme?.fonts?.heading ?? "Calibri"));
|
|
2161
|
+
const body = Xml.attr(String(deck?.theme?.fonts?.body ?? "Calibri"));
|
|
2162
|
+
const palette = [...CHART_PALETTE];
|
|
2163
|
+
palette[0] = accent;
|
|
2164
|
+
return Xml.declaration() + '<a:theme xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" name="DarkSlide"><a:themeElements><a:clrScheme name="DarkSlide"><a:dk1><a:srgbClr val="' + text + '"/></a:dk1><a:lt1><a:srgbClr val="' + bg + '"/></a:lt1><a:dk2><a:srgbClr val="' + muted + '"/></a:dk2><a:lt2><a:srgbClr val="' + surface + '"/></a:lt2><a:accent1><a:srgbClr val="' + palette[0] + '"/></a:accent1><a:accent2><a:srgbClr val="' + palette[1] + '"/></a:accent2><a:accent3><a:srgbClr val="' + palette[2] + '"/></a:accent3><a:accent4><a:srgbClr val="' + palette[3] + '"/></a:accent4><a:accent5><a:srgbClr val="' + palette[4] + '"/></a:accent5><a:accent6><a:srgbClr val="' + palette[5] + '"/></a:accent6><a:hlink><a:srgbClr val="0563C1"/></a:hlink><a:folHlink><a:srgbClr val="954F72"/></a:folHlink></a:clrScheme><a:fontScheme name="DarkSlide"><a:majorFont><a:latin typeface="' + heading + '"/><a:ea typeface=""/><a:cs typeface=""/></a:majorFont><a:minorFont><a:latin typeface="' + body + '"/><a:ea typeface=""/><a:cs typeface=""/></a:minorFont></a:fontScheme><a:fmtScheme name="DarkSlide"><a:fillStyleLst><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:solidFill><a:schemeClr val="phClr"/></a:solidFill></a:fillStyleLst><a:lnStyleLst><a:ln w="6350"><a:solidFill><a:schemeClr val="phClr"/></a:solidFill></a:ln><a:ln w="12700"><a:solidFill><a:schemeClr val="phClr"/></a:solidFill></a:ln><a:ln w="19050"><a:solidFill><a:schemeClr val="phClr"/></a:solidFill></a:ln></a:lnStyleLst><a:effectStyleLst><a:effectStyle><a:effectLst/></a:effectStyle><a:effectStyle><a:effectLst/></a:effectStyle><a:effectStyle><a:effectLst/></a:effectStyle></a:effectStyleLst><a:bgFillStyleLst><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:solidFill><a:schemeClr val="phClr"/></a:solidFill></a:bgFillStyleLst></a:fmtScheme></a:themeElements></a:theme>';
|
|
2165
|
+
}
|
|
2166
|
+
buildSlideMaster() {
|
|
2167
|
+
return Xml.declaration() + '<p:sldMaster xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"><p:cSld><p:bg><p:bgRef idx="1001"><a:schemeClr val="bg1"/></p:bgRef></p:bg><p:spTree><p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr><p:grpSpPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="0" cy="0"/><a:chOff x="0" y="0"/><a:chExt cx="0" cy="0"/></a:xfrm></p:grpSpPr></p:spTree></p:cSld><p:clrMap bg1="lt1" tx1="dk1" bg2="lt2" tx2="dk2" accent1="accent1" accent2="accent2" accent3="accent3" accent4="accent4" accent5="accent5" accent6="accent6" hlink="hlink" folHlink="folHlink"/><p:sldLayoutIdLst>' + this.buildSlideLayoutIdLst() + '</p:sldLayoutIdLst><p:txStyles><p:titleStyle><a:lvl1pPr algn="ctr"><a:defRPr sz="4400"><a:solidFill><a:schemeClr val="tx1"/></a:solidFill></a:defRPr></a:lvl1pPr></p:titleStyle><p:bodyStyle><a:lvl1pPr><a:defRPr sz="2400"><a:solidFill><a:schemeClr val="tx1"/></a:solidFill></a:defRPr></a:lvl1pPr></p:bodyStyle><p:otherStyle/></p:txStyles></p:sldMaster>';
|
|
2168
|
+
}
|
|
2169
|
+
buildSlideLayoutIdLst() {
|
|
2170
|
+
let out = "";
|
|
2171
|
+
LAYOUT_ORDER.forEach((_, idx) => {
|
|
2172
|
+
const n = idx + 1;
|
|
2173
|
+
const id = 2147483648 + n;
|
|
2174
|
+
out += '<p:sldLayoutId id="' + id + '" r:id="rId' + n + '"/>';
|
|
2175
|
+
});
|
|
2176
|
+
return out;
|
|
2177
|
+
}
|
|
2178
|
+
buildSlideMasterRels() {
|
|
2179
|
+
let rels = "";
|
|
2180
|
+
LAYOUT_ORDER.forEach((_, idx) => {
|
|
2181
|
+
const n = idx + 1;
|
|
2182
|
+
rels += '<Relationship Id="rId' + n + '" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideLayout" Target="../slideLayouts/slideLayout' + n + '.xml"/>';
|
|
2183
|
+
});
|
|
2184
|
+
const themeRid = "rId" + (LAYOUT_ORDER.length + 1);
|
|
2185
|
+
rels += '<Relationship Id="' + themeRid + '" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme" Target="../theme/theme1.xml"/>';
|
|
2186
|
+
return Xml.declaration() + '<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">' + rels + "</Relationships>";
|
|
2187
|
+
}
|
|
2188
|
+
layoutNumberFor(layout) {
|
|
2189
|
+
if (layout === null || layout === void 0) {
|
|
2190
|
+
return 1;
|
|
2191
|
+
}
|
|
2192
|
+
const idx = LAYOUT_ORDER.indexOf(layout);
|
|
2193
|
+
return idx === -1 ? 1 : idx + 1;
|
|
2194
|
+
}
|
|
2195
|
+
layoutTypeFor(layout) {
|
|
2196
|
+
switch (layout) {
|
|
2197
|
+
case "title":
|
|
2198
|
+
return "title";
|
|
2199
|
+
case "title-content":
|
|
2200
|
+
return "obj";
|
|
2201
|
+
case "two-column":
|
|
2202
|
+
return "twoObj";
|
|
2203
|
+
case "section-divider":
|
|
2204
|
+
return "secHead";
|
|
2205
|
+
case "image-text":
|
|
2206
|
+
case "text-image":
|
|
2207
|
+
return "picTx";
|
|
2208
|
+
case "quote":
|
|
2209
|
+
return "obj";
|
|
2210
|
+
default:
|
|
2211
|
+
return "blank";
|
|
2212
|
+
}
|
|
2213
|
+
}
|
|
2214
|
+
buildSlideLayout(layout) {
|
|
2215
|
+
const type = this.layoutTypeFor(layout);
|
|
2216
|
+
const name = Xml.attr(ucwords(layout.replace(/-/g, " ")));
|
|
2217
|
+
return Xml.declaration() + '<p:sldLayout xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main" type="' + type + '" preserve="1"><p:cSld name="' + name + '"><p:spTree><p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr><p:grpSpPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="0" cy="0"/><a:chOff x="0" y="0"/><a:chExt cx="0" cy="0"/></a:xfrm></p:grpSpPr></p:spTree></p:cSld><p:clrMapOvr><a:masterClrMapping/></p:clrMapOvr></p:sldLayout>';
|
|
2218
|
+
}
|
|
2219
|
+
buildSlideLayoutRels() {
|
|
2220
|
+
return Xml.declaration() + '<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideMaster" Target="../slideMasters/slideMaster1.xml"/></Relationships>';
|
|
2221
|
+
}
|
|
2222
|
+
// ─── Per-slide rendering ──────────────────────────────────────────────
|
|
2223
|
+
buildSlideXml(slide, slideNumber, deck = {}) {
|
|
2224
|
+
const elements = [...slide?.elements ?? []];
|
|
2225
|
+
elements.sort((a, b) => (a?.z ?? -1) - (b?.z ?? -1));
|
|
2226
|
+
let shapeId = 2;
|
|
2227
|
+
let shapeTreeXml = "";
|
|
2228
|
+
let slideRels = [];
|
|
2229
|
+
const bgResult = this.buildBackground(slide?.background ?? null, slideNumber, slideRels);
|
|
2230
|
+
const bg = bgResult.xml;
|
|
2231
|
+
slideRels = bgResult.rels;
|
|
2232
|
+
const animatedBuilds = [];
|
|
2233
|
+
elements.forEach((element, arrayIndex) => {
|
|
2234
|
+
if (!isPlainObject(element) || element.type === void 0) {
|
|
2235
|
+
return;
|
|
2236
|
+
}
|
|
2237
|
+
if (element.hidden) {
|
|
2238
|
+
return;
|
|
2239
|
+
}
|
|
2240
|
+
const [xml, rels] = this.buildElementXml(element, shapeId, slideNumber);
|
|
2241
|
+
if (xml === "") {
|
|
2242
|
+
return;
|
|
2243
|
+
}
|
|
2244
|
+
shapeTreeXml += xml;
|
|
2245
|
+
slideRels = slideRels.concat(rels);
|
|
2246
|
+
if (isPlainObject(element.animation) && element.animation.effect !== void 0) {
|
|
2247
|
+
let paragraphCount = null;
|
|
2248
|
+
if (element.type === "text" && element.animation.byParagraph) {
|
|
2249
|
+
paragraphCount = String(element.content ?? "").split("\n").length;
|
|
2250
|
+
}
|
|
2251
|
+
animatedBuilds.push({
|
|
2252
|
+
shapeId,
|
|
2253
|
+
arrayIndex,
|
|
2254
|
+
animation: element.animation,
|
|
2255
|
+
paragraphCount
|
|
2256
|
+
});
|
|
2257
|
+
}
|
|
2258
|
+
shapeId++;
|
|
2259
|
+
});
|
|
2260
|
+
this.pendingSlideRels[slideNumber] = slideRels;
|
|
2261
|
+
const transition = this.buildTransition(slide?.transition ?? null, deck?.theme?.defaultTransition ?? null);
|
|
2262
|
+
const timing = this.buildTiming(animatedBuilds);
|
|
2263
|
+
return Xml.declaration() + '<p:sld xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"><p:cSld>' + bg + '<p:spTree><p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr><p:grpSpPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="0" cy="0"/><a:chOff x="0" y="0"/><a:chExt cx="0" cy="0"/></a:xfrm></p:grpSpPr>' + shapeTreeXml + "</p:spTree></p:cSld>" + transition + timing + "</p:sld>";
|
|
2264
|
+
}
|
|
2265
|
+
buildTransition(transition, fallback) {
|
|
2266
|
+
let spec = isPlainObject(transition) ? transition : null;
|
|
2267
|
+
if (spec === null || (spec.kind ?? null) === null || (spec.kind ?? null) === "none") {
|
|
2268
|
+
spec = isPlainObject(fallback) ? fallback : null;
|
|
2269
|
+
}
|
|
2270
|
+
if (spec === null) {
|
|
2271
|
+
return "";
|
|
2272
|
+
}
|
|
2273
|
+
const kind = typeof spec.kind === "string" ? String(spec.kind).toLowerCase() : "none";
|
|
2274
|
+
if (kind === "none" || !Schema.SLIDE_TRANSITION_KINDS.includes(kind)) {
|
|
2275
|
+
return "";
|
|
2276
|
+
}
|
|
2277
|
+
const spd = this.transitionSpeed(spec.duration ?? null);
|
|
2278
|
+
let effect;
|
|
2279
|
+
switch (kind) {
|
|
2280
|
+
case "fade":
|
|
2281
|
+
effect = "<p:fade/>";
|
|
2282
|
+
break;
|
|
2283
|
+
case "slide":
|
|
2284
|
+
effect = '<p:push dir="' + this.transitionDirection(spec.direction ?? null) + '"/>';
|
|
2285
|
+
break;
|
|
2286
|
+
case "zoom":
|
|
2287
|
+
effect = "<p:circle/>";
|
|
2288
|
+
break;
|
|
2289
|
+
default:
|
|
2290
|
+
effect = "";
|
|
2291
|
+
}
|
|
2292
|
+
if (effect === "") {
|
|
2293
|
+
return "";
|
|
2294
|
+
}
|
|
2295
|
+
return '<p:transition spd="' + spd + '">' + effect + "</p:transition>";
|
|
2296
|
+
}
|
|
2297
|
+
transitionSpeed(duration) {
|
|
2298
|
+
if (!isNumeric(duration)) {
|
|
2299
|
+
return "med";
|
|
2300
|
+
}
|
|
2301
|
+
const ms = parseFloat(String(duration));
|
|
2302
|
+
if (ms >= 700) {
|
|
2303
|
+
return "slow";
|
|
2304
|
+
}
|
|
2305
|
+
if (ms <= 250) {
|
|
2306
|
+
return "fast";
|
|
2307
|
+
}
|
|
2308
|
+
return "med";
|
|
2309
|
+
}
|
|
2310
|
+
transitionDirection(direction) {
|
|
2311
|
+
switch (typeof direction === "string" ? direction.toLowerCase() : "") {
|
|
2312
|
+
case "left":
|
|
2313
|
+
return "l";
|
|
2314
|
+
case "right":
|
|
2315
|
+
return "r";
|
|
2316
|
+
case "up":
|
|
2317
|
+
return "u";
|
|
2318
|
+
case "down":
|
|
2319
|
+
return "d";
|
|
2320
|
+
default:
|
|
2321
|
+
return "l";
|
|
2322
|
+
}
|
|
2323
|
+
}
|
|
2324
|
+
// ─── Element entrance animations (`<p:timing>`) ───────────────────────────
|
|
2325
|
+
buildTiming(builds) {
|
|
2326
|
+
if (builds.length === 0) {
|
|
2327
|
+
return "";
|
|
2328
|
+
}
|
|
2329
|
+
const sorted = [...builds];
|
|
2330
|
+
sorted.sort((a, b) => {
|
|
2331
|
+
const ao = isNumeric(a.animation.order) ? parseFloat(String(a.animation.order)) : 0;
|
|
2332
|
+
const bo = isNumeric(b.animation.order) ? parseFloat(String(b.animation.order)) : 0;
|
|
2333
|
+
if (ao !== bo) {
|
|
2334
|
+
return ao < bo ? -1 : 1;
|
|
2335
|
+
}
|
|
2336
|
+
return a.arrayIndex - b.arrayIndex;
|
|
2337
|
+
});
|
|
2338
|
+
const subBuilds = [];
|
|
2339
|
+
for (const build of sorted) {
|
|
2340
|
+
const paragraphCount = build.paragraphCount ?? null;
|
|
2341
|
+
if (paragraphCount === null || paragraphCount <= 1) {
|
|
2342
|
+
subBuilds.push({
|
|
2343
|
+
shapeId: build.shapeId,
|
|
2344
|
+
arrayIndex: build.arrayIndex,
|
|
2345
|
+
animation: build.animation,
|
|
2346
|
+
paragraph: paragraphCount === null ? null : 0
|
|
2347
|
+
});
|
|
2348
|
+
continue;
|
|
2349
|
+
}
|
|
2350
|
+
for (let i = 0; i < paragraphCount; i++) {
|
|
2351
|
+
const animation = { ...build.animation };
|
|
2352
|
+
if (i > 0) {
|
|
2353
|
+
animation.trigger = "on-click";
|
|
2354
|
+
}
|
|
2355
|
+
subBuilds.push({
|
|
2356
|
+
shapeId: build.shapeId,
|
|
2357
|
+
arrayIndex: build.arrayIndex,
|
|
2358
|
+
animation,
|
|
2359
|
+
paragraph: i
|
|
2360
|
+
});
|
|
2361
|
+
}
|
|
2362
|
+
}
|
|
2363
|
+
const steps = [];
|
|
2364
|
+
for (const build of subBuilds) {
|
|
2365
|
+
const trigger = this.animationTrigger(build.animation.trigger ?? null);
|
|
2366
|
+
if (steps.length === 0 || trigger === "on-click") {
|
|
2367
|
+
steps.push([build]);
|
|
2368
|
+
} else {
|
|
2369
|
+
steps[steps.length - 1].push(build);
|
|
2370
|
+
}
|
|
2371
|
+
}
|
|
2372
|
+
this.tnId = 1;
|
|
2373
|
+
const rootId = this.tnId++;
|
|
2374
|
+
let stepPars = "";
|
|
2375
|
+
for (const step of steps) {
|
|
2376
|
+
stepPars += this.buildStepPar(step);
|
|
2377
|
+
}
|
|
2378
|
+
const mainSeqId = this.tnId++;
|
|
2379
|
+
return '<p:timing><p:tnLst><p:par><p:cTn id="' + rootId + '" dur="indefinite" restart="never" nodeType="tmRoot"><p:childTnLst><p:seq concurrent="1" nextAc="seek"><p:cTn id="' + mainSeqId + '" dur="indefinite" nodeType="mainSeq"><p:childTnLst>' + stepPars + '</p:childTnLst></p:cTn><p:prevCondLst><p:cond evt="onPrev" delay="0"><p:tgtEl><p:sldTgt/></p:tgtEl></p:cond></p:prevCondLst><p:nextCondLst><p:cond evt="onNext" delay="0"><p:tgtEl><p:sldTgt/></p:tgtEl></p:cond></p:nextCondLst></p:seq></p:childTnLst></p:cTn></p:par></p:tnLst></p:timing>';
|
|
2380
|
+
}
|
|
2381
|
+
buildTargetEl(spid, paragraph) {
|
|
2382
|
+
if (paragraph === null) {
|
|
2383
|
+
return '<p:tgtEl><p:spTgt spid="' + spid + '"/></p:tgtEl>';
|
|
2384
|
+
}
|
|
2385
|
+
return '<p:tgtEl><p:spTgt spid="' + spid + '"><p:txEl><p:pRg st="' + paragraph + '" end="' + paragraph + '"/></p:txEl></p:spTgt></p:tgtEl>';
|
|
2386
|
+
}
|
|
2387
|
+
buildStepPar(step) {
|
|
2388
|
+
const lead = step[0];
|
|
2389
|
+
const leadDelay = this.animationDelay(lead.animation);
|
|
2390
|
+
const leadDuration = this.animationDuration(lead.animation);
|
|
2391
|
+
let childTns = "";
|
|
2392
|
+
step.forEach((build, i) => {
|
|
2393
|
+
let begin;
|
|
2394
|
+
if (i === 0) {
|
|
2395
|
+
begin = leadDelay;
|
|
2396
|
+
} else {
|
|
2397
|
+
const trigger = this.animationTrigger(build.animation.trigger ?? null);
|
|
2398
|
+
const base = trigger === "after-prev" ? leadDelay + leadDuration : leadDelay;
|
|
2399
|
+
begin = base + this.animationDelay(build.animation);
|
|
2400
|
+
}
|
|
2401
|
+
childTns += this.buildEffectPar(build, begin);
|
|
2402
|
+
});
|
|
2403
|
+
const stepId = this.tnId++;
|
|
2404
|
+
return '<p:par><p:cTn id="' + stepId + '" fill="hold"><p:stCondLst><p:cond delay="indefinite"/></p:stCondLst><p:childTnLst>' + childTns + "</p:childTnLst></p:cTn></p:par>";
|
|
2405
|
+
}
|
|
2406
|
+
buildEffectPar(build, beginMs) {
|
|
2407
|
+
const spid = build.shapeId;
|
|
2408
|
+
const paragraph = build.paragraph;
|
|
2409
|
+
const animation = build.animation;
|
|
2410
|
+
const effect = this.animationEffect(animation.effect ?? null);
|
|
2411
|
+
const duration = this.animationDuration(animation);
|
|
2412
|
+
const direction = this.animationDirection(animation.direction ?? null);
|
|
2413
|
+
const wrapId = this.tnId++;
|
|
2414
|
+
const stCond = '<p:stCondLst><p:cond delay="' + Math.max(0, beginMs) + '"/></p:stCondLst>';
|
|
2415
|
+
let effectXml;
|
|
2416
|
+
switch (effect) {
|
|
2417
|
+
case "fly-in":
|
|
2418
|
+
effectXml = this.buildFlyInEffect(spid, duration, direction, paragraph);
|
|
2419
|
+
break;
|
|
2420
|
+
case "zoom":
|
|
2421
|
+
effectXml = this.buildZoomEffect(spid, duration, paragraph);
|
|
2422
|
+
break;
|
|
2423
|
+
case "wipe":
|
|
2424
|
+
effectXml = this.buildWipeEffect(spid, duration, direction, paragraph);
|
|
2425
|
+
break;
|
|
2426
|
+
default:
|
|
2427
|
+
effectXml = this.buildFadeEffect(spid, duration, paragraph);
|
|
2428
|
+
}
|
|
2429
|
+
const presetId = this.animationPresetId(effect);
|
|
2430
|
+
return '<p:par><p:cTn id="' + wrapId + '" presetID="' + presetId + '" presetClass="entr" presetSubtype="0" fill="hold">' + stCond + "<p:childTnLst>" + effectXml + "</p:childTnLst></p:cTn></p:par>";
|
|
2431
|
+
}
|
|
2432
|
+
animationPresetId(effect) {
|
|
2433
|
+
switch (effect) {
|
|
2434
|
+
case "fly-in":
|
|
2435
|
+
return 2;
|
|
2436
|
+
case "wipe":
|
|
2437
|
+
return 22;
|
|
2438
|
+
case "zoom":
|
|
2439
|
+
return 23;
|
|
2440
|
+
default:
|
|
2441
|
+
return 10;
|
|
2442
|
+
}
|
|
2443
|
+
}
|
|
2444
|
+
buildVisibilitySet(spid, paragraph = null) {
|
|
2445
|
+
const id = this.tnId++;
|
|
2446
|
+
return '<p:set><p:cBhvr><p:cTn id="' + id + '" dur="1" fill="hold"><p:stCondLst><p:cond delay="0"/></p:stCondLst></p:cTn>' + this.buildTargetEl(spid, paragraph) + '<p:attrNameLst><p:attrName>style.visibility</p:attrName></p:attrNameLst></p:cBhvr><p:to><p:strVal val="visible"/></p:to></p:set>';
|
|
2447
|
+
}
|
|
2448
|
+
buildFadeEffect(spid, durationMs, paragraph = null) {
|
|
2449
|
+
const set = this.buildVisibilitySet(spid, paragraph);
|
|
2450
|
+
const id = this.tnId++;
|
|
2451
|
+
const effect = '<p:animEffect transition="in" filter="fade"><p:cBhvr><p:cTn id="' + id + '" dur="' + durationMs + '"/>' + this.buildTargetEl(spid, paragraph) + "</p:cBhvr></p:animEffect>";
|
|
2452
|
+
return set + effect;
|
|
2453
|
+
}
|
|
2454
|
+
buildFlyInEffect(spid, durationMs, direction, paragraph = null) {
|
|
2455
|
+
const set = this.buildVisibilitySet(spid, paragraph);
|
|
2456
|
+
let attr, fromExpr, toExpr;
|
|
2457
|
+
switch (direction) {
|
|
2458
|
+
case "right":
|
|
2459
|
+
[attr, fromExpr, toExpr] = ["ppt_x", "1+#ppt_w/2", "#ppt_x"];
|
|
2460
|
+
break;
|
|
2461
|
+
case "up":
|
|
2462
|
+
[attr, fromExpr, toExpr] = ["ppt_y", "0-#ppt_h/2", "#ppt_y"];
|
|
2463
|
+
break;
|
|
2464
|
+
case "down":
|
|
2465
|
+
[attr, fromExpr, toExpr] = ["ppt_y", "1+#ppt_h/2", "#ppt_y"];
|
|
2466
|
+
break;
|
|
2467
|
+
default:
|
|
2468
|
+
[attr, fromExpr, toExpr] = ["ppt_x", "0-#ppt_w/2", "#ppt_x"];
|
|
2469
|
+
}
|
|
2470
|
+
const id = this.tnId++;
|
|
2471
|
+
const anim = '<p:anim calcmode="lin" valueType="num"><p:cBhvr additive="base"><p:cTn id="' + id + '" dur="' + durationMs + '" fill="hold"/>' + this.buildTargetEl(spid, paragraph) + "<p:attrNameLst><p:attrName>" + attr + '</p:attrName></p:attrNameLst></p:cBhvr><p:tavLst><p:tav tm="0"><p:val><p:strVal val="' + fromExpr + '"/></p:val></p:tav><p:tav tm="100000"><p:val><p:strVal val="' + toExpr + '"/></p:val></p:tav></p:tavLst></p:anim>';
|
|
2472
|
+
return set + anim;
|
|
2473
|
+
}
|
|
2474
|
+
buildZoomEffect(spid, durationMs, paragraph = null) {
|
|
2475
|
+
const set = this.buildVisibilitySet(spid, paragraph);
|
|
2476
|
+
const fadeId = this.tnId++;
|
|
2477
|
+
const scaleId = this.tnId++;
|
|
2478
|
+
const fade = '<p:animEffect transition="in" filter="fade"><p:cBhvr><p:cTn id="' + fadeId + '" dur="' + durationMs + '"/>' + this.buildTargetEl(spid, paragraph) + "</p:cBhvr></p:animEffect>";
|
|
2479
|
+
const scale = '<p:animScale><p:cBhvr><p:cTn id="' + scaleId + '" dur="' + durationMs + '" fill="hold"/>' + this.buildTargetEl(spid, paragraph) + '</p:cBhvr><p:from x="0" y="0"/><p:to x="100000" y="100000"/></p:animScale>';
|
|
2480
|
+
return set + fade + scale;
|
|
2481
|
+
}
|
|
2482
|
+
buildWipeEffect(spid, durationMs, direction, paragraph = null) {
|
|
2483
|
+
const set = this.buildVisibilitySet(spid, paragraph);
|
|
2484
|
+
let filter;
|
|
2485
|
+
switch (direction) {
|
|
2486
|
+
case "right":
|
|
2487
|
+
filter = "wipe(left)";
|
|
2488
|
+
break;
|
|
2489
|
+
case "up":
|
|
2490
|
+
filter = "wipe(down)";
|
|
2491
|
+
break;
|
|
2492
|
+
case "down":
|
|
2493
|
+
filter = "wipe(up)";
|
|
2494
|
+
break;
|
|
2495
|
+
default:
|
|
2496
|
+
filter = "wipe(right)";
|
|
2497
|
+
}
|
|
2498
|
+
const id = this.tnId++;
|
|
2499
|
+
const effect = '<p:animEffect transition="in" filter="' + filter + '"><p:cBhvr><p:cTn id="' + id + '" dur="' + durationMs + '"/>' + this.buildTargetEl(spid, paragraph) + "</p:cBhvr></p:animEffect>";
|
|
2500
|
+
return set + effect;
|
|
2501
|
+
}
|
|
2502
|
+
animationEffect(effect) {
|
|
2503
|
+
const name = typeof effect === "string" ? effect.toLowerCase() : "";
|
|
2504
|
+
return Schema.ANIMATION_EFFECTS.includes(name) ? name : "fade";
|
|
2505
|
+
}
|
|
2506
|
+
animationTrigger(trigger) {
|
|
2507
|
+
const name = typeof trigger === "string" ? trigger.toLowerCase() : "";
|
|
2508
|
+
return Schema.ANIMATION_TRIGGERS.includes(name) ? name : "on-click";
|
|
2509
|
+
}
|
|
2510
|
+
animationDirection(direction) {
|
|
2511
|
+
const name = typeof direction === "string" ? direction.toLowerCase() : "";
|
|
2512
|
+
return Schema.ANIMATION_DIRECTIONS.includes(name) ? name : "left";
|
|
2513
|
+
}
|
|
2514
|
+
animationDuration(animation) {
|
|
2515
|
+
const duration = isNumeric(animation.duration) ? Math.round(parseFloat(String(animation.duration))) : Schema.ANIMATION_DEFAULT_DURATION_MS;
|
|
2516
|
+
return Math.max(1, duration);
|
|
2517
|
+
}
|
|
2518
|
+
animationDelay(animation) {
|
|
2519
|
+
const delay = isNumeric(animation.delay) ? Math.round(parseFloat(String(animation.delay))) : 0;
|
|
2520
|
+
return Math.max(0, delay);
|
|
2521
|
+
}
|
|
2522
|
+
// ─── Background ───────────────────────────────────────────────────────
|
|
2523
|
+
buildBackground(bg, slideNumber, rels) {
|
|
2524
|
+
if (!isPlainObject(bg)) {
|
|
2525
|
+
return { xml: "", rels };
|
|
2526
|
+
}
|
|
2527
|
+
if (typeof bg.image === "string" && bg.image !== "") {
|
|
2528
|
+
const embed = this.stageMedia(bg.image, slideNumber);
|
|
2529
|
+
if (embed !== null) {
|
|
2530
|
+
rels.push({
|
|
2531
|
+
id: embed.relId,
|
|
2532
|
+
type: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image",
|
|
2533
|
+
target: embed.target
|
|
2534
|
+
});
|
|
2535
|
+
return {
|
|
2536
|
+
xml: '<p:bg><p:bgPr><a:blipFill dpi="0" rotWithShape="1"><a:blip r:embed="' + embed.relId + '"/><a:srcRect/><a:stretch><a:fillRect/></a:stretch></a:blipFill><a:effectLst/></p:bgPr></p:bg>',
|
|
2537
|
+
rels
|
|
2538
|
+
};
|
|
2539
|
+
}
|
|
2540
|
+
}
|
|
2541
|
+
if (typeof bg.gradient === "string") {
|
|
2542
|
+
const grad = this.parseGradient(bg.gradient);
|
|
2543
|
+
if (grad !== null) {
|
|
2544
|
+
return { xml: "<p:bg><p:bgPr>" + grad + "<a:effectLst/></p:bgPr></p:bg>", rels };
|
|
2545
|
+
}
|
|
2546
|
+
}
|
|
2547
|
+
if (typeof bg.color === "string") {
|
|
2548
|
+
const [hex, alpha] = Color.parse(bg.color);
|
|
2549
|
+
return {
|
|
2550
|
+
xml: '<p:bg><p:bgPr><a:solidFill><a:srgbClr val="' + hex + '"><a:alpha val="' + alpha + '"/></a:srgbClr></a:solidFill><a:effectLst/></p:bgPr></p:bg>',
|
|
2551
|
+
rels
|
|
2552
|
+
};
|
|
2553
|
+
}
|
|
2554
|
+
return { xml: "", rels };
|
|
2555
|
+
}
|
|
2556
|
+
parseGradient(cssInput) {
|
|
2557
|
+
const css = cssInput.trim();
|
|
2558
|
+
const m = /^linear-gradient\((.+)\)\s*;?\s*$/i.exec(css);
|
|
2559
|
+
if (!m) {
|
|
2560
|
+
return null;
|
|
2561
|
+
}
|
|
2562
|
+
const args = m[1];
|
|
2563
|
+
const parts = this.splitTopLevelCommas(args);
|
|
2564
|
+
if (parts.length < 2) {
|
|
2565
|
+
return null;
|
|
2566
|
+
}
|
|
2567
|
+
let angleDeg = 90;
|
|
2568
|
+
const first = parts[0].trim();
|
|
2569
|
+
const directionLike = /^(?:to\s+|[-+]?[0-9.]+(?:deg|rad|turn|grad)?\s*$)/i.test(first);
|
|
2570
|
+
if (directionLike) {
|
|
2571
|
+
angleDeg = this.parseGradientDirection(first);
|
|
2572
|
+
parts.shift();
|
|
2573
|
+
}
|
|
2574
|
+
const stops = [];
|
|
2575
|
+
const count = parts.length;
|
|
2576
|
+
parts.forEach((partRaw, i) => {
|
|
2577
|
+
const part = partRaw.trim();
|
|
2578
|
+
let colorStr;
|
|
2579
|
+
let pos;
|
|
2580
|
+
const sm = /^(.+?)\s+([0-9.]+%?)\s*$/.exec(part);
|
|
2581
|
+
if (sm) {
|
|
2582
|
+
colorStr = sm[1];
|
|
2583
|
+
const posStr = sm[2];
|
|
2584
|
+
if (posStr.endsWith("%")) {
|
|
2585
|
+
pos = parseFloat(posStr.replace(/%+$/, "")) / 100;
|
|
2586
|
+
} else {
|
|
2587
|
+
pos = parseFloat(posStr);
|
|
2588
|
+
}
|
|
2589
|
+
} else {
|
|
2590
|
+
colorStr = part;
|
|
2591
|
+
pos = count <= 1 ? 0 : i / (count - 1);
|
|
2592
|
+
}
|
|
2593
|
+
const [hex] = Color.parse(colorStr);
|
|
2594
|
+
stops.push({ hex, pos: Math.max(0, Math.min(1, pos)) });
|
|
2595
|
+
});
|
|
2596
|
+
let gsList = "";
|
|
2597
|
+
for (const stop of stops) {
|
|
2598
|
+
const pos1000 = Math.round(stop.pos * 1e5);
|
|
2599
|
+
gsList += '<a:gs pos="' + pos1000 + '"><a:srgbClr val="' + stop.hex + '"/></a:gs>';
|
|
2600
|
+
}
|
|
2601
|
+
let pptxAngle = Math.round((angleDeg - 90) * 6e4);
|
|
2602
|
+
pptxAngle = (pptxAngle % (360 * 6e4) + 360 * 6e4) % (360 * 6e4);
|
|
2603
|
+
return '<a:gradFill flip="none" rotWithShape="1"><a:gsLst>' + gsList + '</a:gsLst><a:lin ang="' + pptxAngle + '" scaled="0"/></a:gradFill>';
|
|
2604
|
+
}
|
|
2605
|
+
splitTopLevelCommas(s) {
|
|
2606
|
+
const out = [];
|
|
2607
|
+
let depth = 0;
|
|
2608
|
+
let buf = "";
|
|
2609
|
+
for (let i = 0; i < s.length; i++) {
|
|
2610
|
+
const c = s[i];
|
|
2611
|
+
if (c === "(") {
|
|
2612
|
+
depth++;
|
|
2613
|
+
buf += c;
|
|
2614
|
+
} else if (c === ")") {
|
|
2615
|
+
depth = Math.max(0, depth - 1);
|
|
2616
|
+
buf += c;
|
|
2617
|
+
} else if (c === "," && depth === 0) {
|
|
2618
|
+
out.push(buf);
|
|
2619
|
+
buf = "";
|
|
2620
|
+
} else {
|
|
2621
|
+
buf += c;
|
|
2622
|
+
}
|
|
2623
|
+
}
|
|
2624
|
+
if (buf !== "") {
|
|
2625
|
+
out.push(buf);
|
|
2626
|
+
}
|
|
2627
|
+
return out;
|
|
2628
|
+
}
|
|
2629
|
+
parseGradientDirection(dirInput) {
|
|
2630
|
+
const dir = dirInput.trim();
|
|
2631
|
+
let m = /^([-+]?[0-9.]+)deg$/i.exec(dir);
|
|
2632
|
+
if (m) {
|
|
2633
|
+
return parseFloat(m[1]);
|
|
2634
|
+
}
|
|
2635
|
+
m = /^([-+]?[0-9.]+)rad$/i.exec(dir);
|
|
2636
|
+
if (m) {
|
|
2637
|
+
return parseFloat(m[1]) * 180 / Math.PI;
|
|
2638
|
+
}
|
|
2639
|
+
m = /^([-+]?[0-9.]+)turn$/i.exec(dir);
|
|
2640
|
+
if (m) {
|
|
2641
|
+
return parseFloat(m[1]) * 360;
|
|
2642
|
+
}
|
|
2643
|
+
switch (dir.toLowerCase()) {
|
|
2644
|
+
case "to top":
|
|
2645
|
+
return 0;
|
|
2646
|
+
case "to top right":
|
|
2647
|
+
return 45;
|
|
2648
|
+
case "to right":
|
|
2649
|
+
return 90;
|
|
2650
|
+
case "to bottom right":
|
|
2651
|
+
return 135;
|
|
2652
|
+
case "to bottom":
|
|
2653
|
+
return 180;
|
|
2654
|
+
case "to bottom left":
|
|
2655
|
+
return 225;
|
|
2656
|
+
case "to left":
|
|
2657
|
+
return 270;
|
|
2658
|
+
case "to top left":
|
|
2659
|
+
return 315;
|
|
2660
|
+
default:
|
|
2661
|
+
return 180;
|
|
2662
|
+
}
|
|
2663
|
+
}
|
|
2664
|
+
// ─── Element dispatch ─────────────────────────────────────────────────
|
|
2665
|
+
buildElementXml(element, shapeId, slideNumber) {
|
|
2666
|
+
const rels = [];
|
|
2667
|
+
let xml;
|
|
2668
|
+
switch (element.type) {
|
|
2669
|
+
case "text":
|
|
2670
|
+
xml = this.buildTextShape(element, shapeId);
|
|
2671
|
+
break;
|
|
2672
|
+
case "image":
|
|
2673
|
+
xml = this.buildImageShape(element, shapeId, slideNumber, rels);
|
|
2674
|
+
break;
|
|
2675
|
+
case "shape":
|
|
2676
|
+
xml = this.buildShape(element, shapeId);
|
|
2677
|
+
break;
|
|
2678
|
+
case "code":
|
|
2679
|
+
xml = this.buildCodeShape(element, shapeId);
|
|
2680
|
+
break;
|
|
2681
|
+
case "chart":
|
|
2682
|
+
xml = this.buildChart(element, shapeId, slideNumber, rels);
|
|
2683
|
+
break;
|
|
2684
|
+
case "table":
|
|
2685
|
+
xml = this.buildTable(element, shapeId);
|
|
2686
|
+
break;
|
|
2687
|
+
case "embed":
|
|
2688
|
+
xml = this.buildPlaceholder("[embed: " + String(element.src ?? "") + "]", element, shapeId);
|
|
2689
|
+
break;
|
|
2690
|
+
default:
|
|
2691
|
+
xml = "";
|
|
2692
|
+
}
|
|
2693
|
+
xml = this.applyHyperlink(xml, element, shapeId, rels);
|
|
2694
|
+
return [xml, rels];
|
|
2695
|
+
}
|
|
2696
|
+
applyHyperlink(xml, element, shapeId, rels) {
|
|
2697
|
+
const href = element.href ?? null;
|
|
2698
|
+
if (typeof href !== "string" || href === "" || xml === "") {
|
|
2699
|
+
return xml;
|
|
2700
|
+
}
|
|
2701
|
+
const relId = "rIdLink" + shapeId;
|
|
2702
|
+
rels.push({
|
|
2703
|
+
id: relId,
|
|
2704
|
+
type: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink",
|
|
2705
|
+
target: href,
|
|
2706
|
+
mode: "External"
|
|
2707
|
+
});
|
|
2708
|
+
const hlink = '<a:hlinkClick r:id="' + relId + '"/>';
|
|
2709
|
+
let count = 0;
|
|
2710
|
+
const injected = xml.replace(/<p:cNvPr\b([^>]*)\/>/, (_full, attrs) => {
|
|
2711
|
+
if (count > 0) return _full;
|
|
2712
|
+
count++;
|
|
2713
|
+
return "<p:cNvPr" + attrs + ">" + hlink + "</p:cNvPr>";
|
|
2714
|
+
});
|
|
2715
|
+
return count > 0 ? injected : xml;
|
|
2716
|
+
}
|
|
2717
|
+
// ─── Element renderers ────────────────────────────────────────────────
|
|
2718
|
+
buildTextShape(element, shapeId) {
|
|
2719
|
+
const xfrm = this.xfrmFromFractions(element);
|
|
2720
|
+
const body = this.buildTextBody(
|
|
2721
|
+
String(element.content ?? ""),
|
|
2722
|
+
element.style ?? {},
|
|
2723
|
+
String(element.format ?? "plain")
|
|
2724
|
+
);
|
|
2725
|
+
const id = element.id ?? `text-${shapeId}`;
|
|
2726
|
+
return '<p:sp><p:nvSpPr><p:cNvPr id="' + shapeId + '" name="' + Xml.attr(String(id)) + '"/><p:cNvSpPr txBox="1"/><p:nvPr/></p:nvSpPr><p:spPr>' + xfrm + '<a:prstGeom prst="rect"><a:avLst/></a:prstGeom><a:noFill/></p:spPr>' + body + "</p:sp>";
|
|
2727
|
+
}
|
|
2728
|
+
buildImageShape(element, shapeId, slideNumber, rels) {
|
|
2729
|
+
const src = String(element.src ?? "");
|
|
2730
|
+
const embed = this.stageMedia(src, slideNumber);
|
|
2731
|
+
if (embed === null) {
|
|
2732
|
+
return this.buildPlaceholder("[image: " + src + "]", element, shapeId);
|
|
2733
|
+
}
|
|
2734
|
+
const relId = embed.relId;
|
|
2735
|
+
rels.push({
|
|
2736
|
+
id: relId,
|
|
2737
|
+
type: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image",
|
|
2738
|
+
target: embed.target
|
|
2739
|
+
});
|
|
2740
|
+
const id = element.id ?? `image-${shapeId}`;
|
|
2741
|
+
const alt = Xml.attr(String(element.alt ?? ""));
|
|
2742
|
+
const fit = typeof element.fit === "string" ? element.fit.toLowerCase() : "fill";
|
|
2743
|
+
const boxX = Emu.fromFracX(toFloat(element.x ?? 0));
|
|
2744
|
+
const boxY = Emu.fromFracY(toFloat(element.y ?? 0));
|
|
2745
|
+
const boxW = Math.max(1, Emu.fromFracX(toFloat(element.w ?? 0)));
|
|
2746
|
+
const boxH = Math.max(1, Emu.fromFracY(toFloat(element.h ?? 0)));
|
|
2747
|
+
const intrinsic = getImageSize(embed.bytes);
|
|
2748
|
+
const imgW = intrinsic ? intrinsic[0] : 0;
|
|
2749
|
+
const imgH = intrinsic ? intrinsic[1] : 0;
|
|
2750
|
+
let offX = boxX;
|
|
2751
|
+
let offY = boxY;
|
|
2752
|
+
let extW = boxW;
|
|
2753
|
+
let extH = boxH;
|
|
2754
|
+
let srcRect = "";
|
|
2755
|
+
const explicitCrop = this.imageCropRect(element.crop ?? null);
|
|
2756
|
+
if (explicitCrop !== null) {
|
|
2757
|
+
srcRect = explicitCrop;
|
|
2758
|
+
} else if (fit === "cover" && imgW > 0 && imgH > 0) {
|
|
2759
|
+
srcRect = this.coverSrcRect(boxW, boxH, imgW, imgH);
|
|
2760
|
+
} else if ((fit === "contain" || fit === "scale-down") && imgW > 0 && imgH > 0) {
|
|
2761
|
+
[offX, offY, extW, extH] = this.containedRect(boxX, boxY, boxW, boxH, imgW, imgH);
|
|
2762
|
+
}
|
|
2763
|
+
const blipFill = '<p:blipFill><a:blip r:embed="' + relId + '"/>' + srcRect + "<a:stretch><a:fillRect/></a:stretch></p:blipFill>";
|
|
2764
|
+
return '<p:pic><p:nvPicPr><p:cNvPr id="' + shapeId + '" name="' + Xml.attr(String(id)) + '" descr="' + alt + '"/><p:cNvPicPr><a:picLocks noChangeAspect="1"/></p:cNvPicPr><p:nvPr/></p:nvPicPr>' + blipFill + '<p:spPr><a:xfrm><a:off x="' + offX + '" y="' + offY + '"/><a:ext cx="' + extW + '" cy="' + extH + '"/></a:xfrm><a:prstGeom prst="rect"><a:avLst/></a:prstGeom></p:spPr></p:pic>';
|
|
2765
|
+
}
|
|
2766
|
+
imageCropRect(crop) {
|
|
2767
|
+
if (!isPlainObject(crop)) {
|
|
2768
|
+
return null;
|
|
2769
|
+
}
|
|
2770
|
+
const x = isNumeric(crop.x) ? parseFloat(String(crop.x)) : null;
|
|
2771
|
+
const y = isNumeric(crop.y) ? parseFloat(String(crop.y)) : null;
|
|
2772
|
+
const w = isNumeric(crop.w) ? parseFloat(String(crop.w)) : null;
|
|
2773
|
+
const h = isNumeric(crop.h) ? parseFloat(String(crop.h)) : null;
|
|
2774
|
+
if (x === null || y === null || w === null || h === null) {
|
|
2775
|
+
return null;
|
|
2776
|
+
}
|
|
2777
|
+
let l = Math.round(x * 1e5);
|
|
2778
|
+
let t = Math.round(y * 1e5);
|
|
2779
|
+
let r = Math.round((1 - x - w) * 1e5);
|
|
2780
|
+
let b = Math.round((1 - y - h) * 1e5);
|
|
2781
|
+
l = Math.max(0, Math.min(1e5, l));
|
|
2782
|
+
t = Math.max(0, Math.min(1e5, t));
|
|
2783
|
+
r = Math.max(0, Math.min(1e5, r));
|
|
2784
|
+
b = Math.max(0, Math.min(1e5, b));
|
|
2785
|
+
return '<a:srcRect l="' + l + '" t="' + t + '" r="' + r + '" b="' + b + '"/>';
|
|
2786
|
+
}
|
|
2787
|
+
coverSrcRect(boxW, boxH, imgW, imgH) {
|
|
2788
|
+
const boxAspect = boxW / boxH;
|
|
2789
|
+
const imgAspect = imgW / imgH;
|
|
2790
|
+
let l = 0, t = 0, r = 0, b = 0;
|
|
2791
|
+
if (imgAspect > boxAspect) {
|
|
2792
|
+
const visibleFrac = boxAspect / imgAspect;
|
|
2793
|
+
const inset = Math.round((1 - visibleFrac) / 2 * 1e5);
|
|
2794
|
+
l = inset;
|
|
2795
|
+
r = inset;
|
|
2796
|
+
} else if (imgAspect < boxAspect) {
|
|
2797
|
+
const visibleFrac = imgAspect / boxAspect;
|
|
2798
|
+
const inset = Math.round((1 - visibleFrac) / 2 * 1e5);
|
|
2799
|
+
t = inset;
|
|
2800
|
+
b = inset;
|
|
2801
|
+
}
|
|
2802
|
+
return '<a:srcRect l="' + l + '" t="' + t + '" r="' + r + '" b="' + b + '"/>';
|
|
2803
|
+
}
|
|
2804
|
+
containedRect(boxX, boxY, boxW, boxH, imgW, imgH) {
|
|
2805
|
+
const scale = Math.min(boxW / imgW, boxH / imgH);
|
|
2806
|
+
const extW = Math.max(1, Math.round(imgW * scale));
|
|
2807
|
+
const extH = Math.max(1, Math.round(imgH * scale));
|
|
2808
|
+
const offX = boxX + Math.round((boxW - extW) / 2);
|
|
2809
|
+
const offY = boxY + Math.round((boxH - extH) / 2);
|
|
2810
|
+
return [offX, offY, extW, extH];
|
|
2811
|
+
}
|
|
2812
|
+
buildShape(element, shapeId) {
|
|
2813
|
+
const xfrm = this.xfrmFromFractions(element);
|
|
2814
|
+
const id = element.id ?? `shape-${shapeId}`;
|
|
2815
|
+
const kind = String(element.shape ?? "rect");
|
|
2816
|
+
let prst;
|
|
2817
|
+
switch (kind) {
|
|
2818
|
+
case "rect":
|
|
2819
|
+
prst = "rect";
|
|
2820
|
+
break;
|
|
2821
|
+
case "rounded-rect":
|
|
2822
|
+
prst = "roundRect";
|
|
2823
|
+
break;
|
|
2824
|
+
case "ellipse":
|
|
2825
|
+
prst = "ellipse";
|
|
2826
|
+
break;
|
|
2827
|
+
case "triangle":
|
|
2828
|
+
prst = "triangle";
|
|
2829
|
+
break;
|
|
2830
|
+
case "line":
|
|
2831
|
+
prst = "line";
|
|
2832
|
+
break;
|
|
2833
|
+
case "arrow":
|
|
2834
|
+
prst = "rightArrow";
|
|
2835
|
+
break;
|
|
2836
|
+
default:
|
|
2837
|
+
prst = "rect";
|
|
2838
|
+
}
|
|
2839
|
+
const [fillHex, fillAlpha] = Color.parse(element.fill ?? "rgba(139,92,246,0.15)", "8B5CF6");
|
|
2840
|
+
const [strokeHex] = Color.parse(element.stroke ?? "#8B5CF6", "8B5CF6");
|
|
2841
|
+
const strokeWidthEmu = Emu.fromPt(toFloat(element.strokeWidth ?? 2));
|
|
2842
|
+
const dashStr = element.dashed ? '<a:prstDash val="dash"/>' : "";
|
|
2843
|
+
const fillXml = fillAlpha === 0 ? "<a:noFill/>" : '<a:solidFill><a:srgbClr val="' + fillHex + '"><a:alpha val="' + fillAlpha + '"/></a:srgbClr></a:solidFill>';
|
|
2844
|
+
return '<p:sp><p:nvSpPr><p:cNvPr id="' + shapeId + '" name="' + Xml.attr(String(id)) + '"/><p:cNvSpPr/><p:nvPr/></p:nvSpPr><p:spPr>' + xfrm + '<a:prstGeom prst="' + prst + '"><a:avLst/></a:prstGeom>' + fillXml + '<a:ln w="' + strokeWidthEmu + '"><a:solidFill><a:srgbClr val="' + strokeHex + '"/></a:solidFill>' + dashStr + '</a:ln></p:spPr><p:txBody><a:bodyPr/><a:lstStyle/><a:p><a:endParaRPr lang="en-US"/></a:p></p:txBody></p:sp>';
|
|
2845
|
+
}
|
|
2846
|
+
buildCodeShape(element, shapeId) {
|
|
2847
|
+
const xfrm = this.xfrmFromFractions(element);
|
|
2848
|
+
const code = String(element.code ?? "");
|
|
2849
|
+
const id = element.id ?? `code-${shapeId}`;
|
|
2850
|
+
const language = element.language !== void 0 && element.language !== null ? String(element.language) : null;
|
|
2851
|
+
const body = this.buildHighlightedCodeBody(code, language);
|
|
2852
|
+
return '<p:sp><p:nvSpPr><p:cNvPr id="' + shapeId + '" name="' + Xml.attr(String(id)) + '"/><p:cNvSpPr txBox="1"/><p:nvPr/></p:nvSpPr><p:spPr>' + xfrm + '<a:prstGeom prst="rect"><a:avLst/></a:prstGeom><a:solidFill><a:srgbClr val="0F172A"/></a:solidFill></p:spPr>' + body + "</p:sp>";
|
|
2853
|
+
}
|
|
2854
|
+
buildHighlightedCodeBody(code, language) {
|
|
2855
|
+
const sz = Emu.hundredthsOfPoint(12);
|
|
2856
|
+
let paragraphs = "";
|
|
2857
|
+
const lines = code.split("\n");
|
|
2858
|
+
for (const line of lines) {
|
|
2859
|
+
const tokens = SyntaxHighlighter.tokenize(line, language);
|
|
2860
|
+
let runs = "";
|
|
2861
|
+
for (const token of tokens) {
|
|
2862
|
+
if (token.text === "") {
|
|
2863
|
+
continue;
|
|
2864
|
+
}
|
|
2865
|
+
const color = SyntaxHighlighter.colorFor(token.kind);
|
|
2866
|
+
runs += '<a:r><a:rPr lang="en-US" sz="' + sz + '"><a:solidFill><a:srgbClr val="' + color + '"/></a:solidFill><a:latin typeface="Consolas"/></a:rPr><a:t>' + Xml.text(token.text) + "</a:t></a:r>";
|
|
2867
|
+
}
|
|
2868
|
+
if (runs === "") {
|
|
2869
|
+
runs = '<a:endParaRPr lang="en-US" sz="' + sz + '"/>';
|
|
2870
|
+
}
|
|
2871
|
+
paragraphs += '<a:p><a:pPr algn="l"/>' + runs + "</a:p>";
|
|
2872
|
+
}
|
|
2873
|
+
return '<p:txBody><a:bodyPr wrap="square" anchor="t" rtlCol="0" lIns="91440" tIns="45720" rIns="91440" bIns="45720"/><a:lstStyle/>' + paragraphs + "</p:txBody>";
|
|
2874
|
+
}
|
|
2875
|
+
buildTable(element, shapeId) {
|
|
2876
|
+
const columns = Array.isArray(element.columns) ? element.columns : [];
|
|
2877
|
+
const rows = Array.isArray(element.rows) ? element.rows : [];
|
|
2878
|
+
if (columns.length === 0) {
|
|
2879
|
+
return this.buildPlaceholder("[table: no columns]", element, shapeId);
|
|
2880
|
+
}
|
|
2881
|
+
const totalWidthEmu = Emu.fromFracX(toFloat(element.w ?? 0.5));
|
|
2882
|
+
const colCount = columns.length;
|
|
2883
|
+
const colWidthEmu = Math.round(totalWidthEmu / Math.max(1, colCount));
|
|
2884
|
+
const headerRowH = Emu.fromPt(40);
|
|
2885
|
+
const bodyRowH = Emu.fromPt(30);
|
|
2886
|
+
let gridCols = "";
|
|
2887
|
+
for (let i = 0; i < columns.length; i++) {
|
|
2888
|
+
gridCols += '<a:gridCol w="' + colWidthEmu + '"/>';
|
|
2889
|
+
}
|
|
2890
|
+
let headerCells = "";
|
|
2891
|
+
for (const col of columns) {
|
|
2892
|
+
const label = String(col.label ?? col.key ?? "");
|
|
2893
|
+
headerCells += this.buildTableCell(label, true);
|
|
2894
|
+
}
|
|
2895
|
+
const headerRow = '<a:tr h="' + headerRowH + '">' + headerCells + "</a:tr>";
|
|
2896
|
+
let bodyRows = "";
|
|
2897
|
+
let rowIndex = 0;
|
|
2898
|
+
for (const row of rows) {
|
|
2899
|
+
if (!isPlainObject(row)) {
|
|
2900
|
+
continue;
|
|
2901
|
+
}
|
|
2902
|
+
let cells = "";
|
|
2903
|
+
for (const col of columns) {
|
|
2904
|
+
const key = String(col.key ?? "");
|
|
2905
|
+
const value = row[key] ?? "";
|
|
2906
|
+
const text = isScalar2(value) ? scalarToString(value) : JSON.stringify(value);
|
|
2907
|
+
cells += this.buildTableCell(String(text), false, rowIndex % 2 === 1);
|
|
2908
|
+
}
|
|
2909
|
+
bodyRows += '<a:tr h="' + bodyRowH + '">' + cells + "</a:tr>";
|
|
2910
|
+
rowIndex++;
|
|
2911
|
+
}
|
|
2912
|
+
const xfrm = this.xfrmFromFractions(element);
|
|
2913
|
+
const id = element.id ?? `table-${shapeId}`;
|
|
2914
|
+
return '<p:graphicFrame><p:nvGraphicFramePr><p:cNvPr id="' + shapeId + '" name="' + Xml.attr(String(id)) + '"/><p:cNvGraphicFramePr><a:graphicFrameLocks noGrp="1"/></p:cNvGraphicFramePr><p:nvPr/></p:nvGraphicFramePr><p:xfrm>' + innerXfrm(xfrm) + '</p:xfrm><a:graphic xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"><a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/table"><a:tbl><a:tblPr firstRow="1" bandRow="1"><a:tableStyleId>{5C22544A-7EE6-4342-B048-85BDC9FD1C3A}</a:tableStyleId></a:tblPr><a:tblGrid>' + gridCols + "</a:tblGrid>" + headerRow + bodyRows + "</a:tbl></a:graphicData></a:graphic></p:graphicFrame>";
|
|
2915
|
+
}
|
|
2916
|
+
buildTableCell(text, header, striped = false) {
|
|
2917
|
+
let fill;
|
|
2918
|
+
let textColor;
|
|
2919
|
+
let bold;
|
|
2920
|
+
if (header) {
|
|
2921
|
+
fill = '<a:solidFill><a:srgbClr val="8B5CF6"/></a:solidFill>';
|
|
2922
|
+
textColor = "FFFFFF";
|
|
2923
|
+
bold = ' b="1"';
|
|
2924
|
+
} else {
|
|
2925
|
+
fill = striped ? '<a:solidFill><a:srgbClr val="F8FAFC"/></a:solidFill>' : "<a:noFill/>";
|
|
2926
|
+
textColor = "0F172A";
|
|
2927
|
+
bold = "";
|
|
2928
|
+
}
|
|
2929
|
+
return '<a:tc><a:txBody><a:bodyPr wrap="square" anchor="ctr" lIns="91440" tIns="45720" rIns="91440" bIns="45720"/><a:lstStyle/><a:p><a:pPr algn="l"/><a:r><a:rPr lang="en-US" sz="1400"' + bold + '><a:solidFill><a:srgbClr val="' + textColor + '"/></a:solidFill></a:rPr><a:t>' + Xml.text(text) + "</a:t></a:r></a:p></a:txBody><a:tcPr>" + fill + "</a:tcPr></a:tc>";
|
|
2930
|
+
}
|
|
2931
|
+
// ─── Charts ───────────────────────────────────────────────────────────
|
|
2932
|
+
buildChart(element, shapeId, slideNumber, rels) {
|
|
2933
|
+
const option = isPlainObject(element.option) ? element.option : null;
|
|
2934
|
+
const spec = option !== null ? ChartTranslator.translate(option) : null;
|
|
2935
|
+
if (spec !== null) {
|
|
2936
|
+
this.chartCounter++;
|
|
2937
|
+
const n = this.chartCounter;
|
|
2938
|
+
const archivePath = `ppt/charts/chart${n}.xml`;
|
|
2939
|
+
this.chartFiles.push({ path: archivePath, xml: this.buildChartPart(spec) });
|
|
2940
|
+
const relId = "rIdChart" + n;
|
|
2941
|
+
rels.push({
|
|
2942
|
+
id: relId,
|
|
2943
|
+
type: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/chart",
|
|
2944
|
+
target: `../charts/chart${n}.xml`
|
|
2945
|
+
});
|
|
2946
|
+
return this.buildChartFrame(element, shapeId, relId);
|
|
2947
|
+
}
|
|
2948
|
+
const preRender = this.chartPreRenderSrc(element);
|
|
2949
|
+
if (preRender !== null) {
|
|
2950
|
+
const imageElement = { ...element };
|
|
2951
|
+
imageElement.src = preRender;
|
|
2952
|
+
imageElement.fit = element.fit ?? "contain";
|
|
2953
|
+
return this.buildImageShape(imageElement, shapeId, slideNumber, rels);
|
|
2954
|
+
}
|
|
2955
|
+
let title = "";
|
|
2956
|
+
if (isPlainObject(option)) {
|
|
2957
|
+
title = String(option.title?.text ?? "");
|
|
2958
|
+
}
|
|
2959
|
+
const label = title !== "" ? title : "chart";
|
|
2960
|
+
return this.buildChartPlaceholder(label, element, shapeId);
|
|
2961
|
+
}
|
|
2962
|
+
chartPreRenderSrc(element) {
|
|
2963
|
+
for (const key of ["image", "src"]) {
|
|
2964
|
+
const value = element[key] ?? null;
|
|
2965
|
+
if (typeof value === "string" && value.startsWith("data:")) {
|
|
2966
|
+
return value;
|
|
2967
|
+
}
|
|
2968
|
+
}
|
|
2969
|
+
return null;
|
|
2970
|
+
}
|
|
2971
|
+
buildChartFrame(element, shapeId, relId) {
|
|
2972
|
+
const xfrm = this.xfrmFromFractions(element);
|
|
2973
|
+
const innerX = innerXfrm(xfrm);
|
|
2974
|
+
const id = element.id ?? `chart-${shapeId}`;
|
|
2975
|
+
return '<p:graphicFrame><p:nvGraphicFramePr><p:cNvPr id="' + shapeId + '" name="' + Xml.attr(String(id)) + '"/><p:cNvGraphicFramePr/><p:nvPr/></p:nvGraphicFramePr><p:xfrm>' + innerX + '</p:xfrm><a:graphic xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"><a:graphicData uri="' + NS_CHART + '"><c:chart xmlns:c="' + NS_CHART + '" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" r:id="' + relId + '"/></a:graphicData></a:graphic></p:graphicFrame>';
|
|
2976
|
+
}
|
|
2977
|
+
buildChartPart(spec) {
|
|
2978
|
+
const kind = spec.kind;
|
|
2979
|
+
let plot;
|
|
2980
|
+
switch (kind) {
|
|
2981
|
+
case "bar":
|
|
2982
|
+
plot = this.buildBarChartXml(spec);
|
|
2983
|
+
break;
|
|
2984
|
+
case "line":
|
|
2985
|
+
plot = this.buildLineChartXml(spec);
|
|
2986
|
+
break;
|
|
2987
|
+
case "pie":
|
|
2988
|
+
plot = this.buildPieChartXml(spec);
|
|
2989
|
+
break;
|
|
2990
|
+
case "scatter":
|
|
2991
|
+
plot = this.buildScatterChartXml(spec);
|
|
2992
|
+
break;
|
|
2993
|
+
default:
|
|
2994
|
+
plot = this.buildBarChartXml(spec);
|
|
2995
|
+
}
|
|
2996
|
+
let title;
|
|
2997
|
+
if (spec.title !== "") {
|
|
2998
|
+
title = "<c:title><c:tx><c:rich><a:bodyPr/><a:p><a:r><a:t>" + Xml.text(spec.title) + '</a:t></a:r></a:p></c:rich></c:tx><c:overlay val="0"/></c:title><c:autoTitleDeleted val="0"/>';
|
|
2999
|
+
} else {
|
|
3000
|
+
title = '<c:autoTitleDeleted val="1"/>';
|
|
3001
|
+
}
|
|
3002
|
+
return Xml.declaration() + '<c:chartSpace xmlns:c="' + NS_CHART + '" xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"><c:chart>' + title + "<c:plotArea><c:layout/>" + plot + '</c:plotArea><c:legend><c:legendPos val="b"/><c:overlay val="0"/></c:legend><c:plotVisOnly val="1"/><c:dispBlanksAs val="gap"/></c:chart></c:chartSpace>';
|
|
3003
|
+
}
|
|
3004
|
+
buildBarChartXml(spec) {
|
|
3005
|
+
let sers = "";
|
|
3006
|
+
spec.series.forEach((series, idx) => {
|
|
3007
|
+
sers += '<c:ser><c:idx val="' + idx + '"/><c:order val="' + idx + '"/>' + this.chartSeriesName(series, idx) + this.chartSeriesFill(idx) + this.chartCatRef(spec.categories, series.values) + this.chartValRef(series.values) + "</c:ser>";
|
|
3008
|
+
});
|
|
3009
|
+
return '<c:barChart><c:barDir val="col"/><c:grouping val="clustered"/><c:varyColors val="0"/>' + sers + '<c:axId val="111111111"/><c:axId val="222222222"/></c:barChart>' + this.buildCatValAxes();
|
|
3010
|
+
}
|
|
3011
|
+
buildLineChartXml(spec) {
|
|
3012
|
+
let isArea = false;
|
|
3013
|
+
for (const series of spec.series) {
|
|
3014
|
+
if (series.area) {
|
|
3015
|
+
isArea = true;
|
|
3016
|
+
break;
|
|
3017
|
+
}
|
|
3018
|
+
}
|
|
3019
|
+
let sers = "";
|
|
3020
|
+
spec.series.forEach((series, idx) => {
|
|
3021
|
+
const smooth = !isArea && series.smooth ? '<c:smooth val="1"/>' : "";
|
|
3022
|
+
sers += '<c:ser><c:idx val="' + idx + '"/><c:order val="' + idx + '"/>' + this.chartSeriesName(series, idx) + this.chartSeriesLine(idx) + this.chartCatRef(spec.categories, series.values) + this.chartValRef(series.values) + smooth + "</c:ser>";
|
|
3023
|
+
});
|
|
3024
|
+
if (isArea) {
|
|
3025
|
+
return '<c:areaChart><c:grouping val="standard"/><c:varyColors val="0"/>' + sers + '<c:axId val="111111111"/><c:axId val="222222222"/></c:areaChart>' + this.buildCatValAxes();
|
|
3026
|
+
}
|
|
3027
|
+
return '<c:lineChart><c:grouping val="standard"/><c:varyColors val="0"/>' + sers + '<c:marker val="1"/><c:axId val="111111111"/><c:axId val="222222222"/></c:lineChart>' + this.buildCatValAxes();
|
|
3028
|
+
}
|
|
3029
|
+
buildPieChartXml(spec) {
|
|
3030
|
+
const series = spec.series[0] ?? { values: [], name: "" };
|
|
3031
|
+
const values = Array.isArray(series.values) ? series.values : [];
|
|
3032
|
+
const categories = spec.categories;
|
|
3033
|
+
let dPts = "";
|
|
3034
|
+
values.forEach((_, idx) => {
|
|
3035
|
+
dPts += '<c:dPt><c:idx val="' + idx + '"/><c:bubble3D val="0"/><c:spPr><a:solidFill><a:srgbClr val="' + this.chartColor(idx) + '"/></a:solidFill></c:spPr></c:dPt>';
|
|
3036
|
+
});
|
|
3037
|
+
return '<c:pieChart><c:varyColors val="1"/><c:ser><c:idx val="0"/><c:order val="0"/>' + this.chartSeriesName(series, 0) + dPts + this.chartCatRef(categories, values) + this.chartValRef(values) + '</c:ser><c:firstSliceAng val="0"/></c:pieChart>';
|
|
3038
|
+
}
|
|
3039
|
+
buildScatterChartXml(spec) {
|
|
3040
|
+
let sers = "";
|
|
3041
|
+
spec.series.forEach((series, idx) => {
|
|
3042
|
+
const points = Array.isArray(series.points) ? series.points : [];
|
|
3043
|
+
const xs = [];
|
|
3044
|
+
const ys = [];
|
|
3045
|
+
for (const point of points) {
|
|
3046
|
+
xs.push(toFloat(point.x ?? 0));
|
|
3047
|
+
ys.push(toFloat(point.y ?? 0));
|
|
3048
|
+
}
|
|
3049
|
+
sers += '<c:ser><c:idx val="' + idx + '"/><c:order val="' + idx + '"/>' + this.chartSeriesName(series, idx) + '<c:spPr><a:ln w="19050"><a:noFill/></a:ln></c:spPr><c:marker><c:symbol val="circle"/><c:size val="6"/><c:spPr><a:solidFill><a:srgbClr val="' + this.chartColor(idx) + '"/></a:solidFill></c:spPr></c:marker><c:xVal>' + this.numLit(xs) + "</c:xVal><c:yVal>" + this.numLit(ys) + "</c:yVal></c:ser>";
|
|
3050
|
+
});
|
|
3051
|
+
return '<c:scatterChart><c:scatterStyle val="lineMarker"/><c:varyColors val="0"/>' + sers + '<c:axId val="111111111"/><c:axId val="222222222"/></c:scatterChart><c:valAx><c:axId val="111111111"/><c:scaling><c:orientation val="minMax"/></c:scaling><c:delete val="0"/><c:axPos val="b"/><c:crossAx val="222222222"/></c:valAx><c:valAx><c:axId val="222222222"/><c:scaling><c:orientation val="minMax"/></c:scaling><c:delete val="0"/><c:axPos val="l"/><c:crossAx val="111111111"/></c:valAx>';
|
|
3052
|
+
}
|
|
3053
|
+
buildCatValAxes() {
|
|
3054
|
+
return '<c:catAx><c:axId val="111111111"/><c:scaling><c:orientation val="minMax"/></c:scaling><c:delete val="0"/><c:axPos val="b"/><c:crossAx val="222222222"/></c:catAx><c:valAx><c:axId val="222222222"/><c:scaling><c:orientation val="minMax"/></c:scaling><c:delete val="0"/><c:axPos val="l"/><c:crossAx val="111111111"/></c:valAx>';
|
|
3055
|
+
}
|
|
3056
|
+
chartSeriesName(series, idx) {
|
|
3057
|
+
const name = typeof series.name === "string" && series.name !== "" ? String(series.name) : "Series " + (idx + 1);
|
|
3058
|
+
return "<c:tx><c:strRef><c:f>Sheet1!$A$" + (idx + 1) + '</c:f><c:strCache><c:ptCount val="1"/><c:pt idx="0"><c:v>' + Xml.text(name) + "</c:v></c:pt></c:strCache></c:strRef></c:tx>";
|
|
3059
|
+
}
|
|
3060
|
+
chartSeriesFill(idx) {
|
|
3061
|
+
return '<c:spPr><a:solidFill><a:srgbClr val="' + this.chartColor(idx) + '"/></a:solidFill></c:spPr>';
|
|
3062
|
+
}
|
|
3063
|
+
chartSeriesLine(idx) {
|
|
3064
|
+
return '<c:spPr><a:ln w="28575"><a:solidFill><a:srgbClr val="' + this.chartColor(idx) + '"/></a:solidFill></a:ln></c:spPr>';
|
|
3065
|
+
}
|
|
3066
|
+
chartCatRef(categoriesInput, values) {
|
|
3067
|
+
let categories = categoriesInput;
|
|
3068
|
+
if (categories.length === 0) {
|
|
3069
|
+
categories = [];
|
|
3070
|
+
for (let i = 0; i < values.length; i++) {
|
|
3071
|
+
categories.push(String(i + 1));
|
|
3072
|
+
}
|
|
3073
|
+
}
|
|
3074
|
+
let pts = "";
|
|
3075
|
+
categories.forEach((label, i) => {
|
|
3076
|
+
pts += '<c:pt idx="' + i + '"><c:v>' + Xml.text(String(label)) + "</c:v></c:pt>";
|
|
3077
|
+
});
|
|
3078
|
+
return '<c:cat><c:strLit><c:ptCount val="' + categories.length + '"/>' + pts + "</c:strLit></c:cat>";
|
|
3079
|
+
}
|
|
3080
|
+
chartValRef(values) {
|
|
3081
|
+
return "<c:val>" + this.numLit(values) + "</c:val>";
|
|
3082
|
+
}
|
|
3083
|
+
numLit(values) {
|
|
3084
|
+
let pts = "";
|
|
3085
|
+
values.forEach((value, i) => {
|
|
3086
|
+
pts += '<c:pt idx="' + i + '"><c:v>' + this.numStr(toFloat(value)) + "</c:v></c:pt>";
|
|
3087
|
+
});
|
|
3088
|
+
return '<c:numLit><c:formatCode>General</c:formatCode><c:ptCount val="' + values.length + '"/>' + pts + "</c:numLit>";
|
|
3089
|
+
}
|
|
3090
|
+
numStr(value) {
|
|
3091
|
+
if (value === Math.floor(value) && Math.abs(value) < 1e15) {
|
|
3092
|
+
return String(Math.trunc(value));
|
|
3093
|
+
}
|
|
3094
|
+
return value.toFixed(6).replace(/0+$/, "").replace(/\.$/, "");
|
|
3095
|
+
}
|
|
3096
|
+
chartColor(idx) {
|
|
3097
|
+
if (idx === 0) {
|
|
3098
|
+
return this.themeAccent;
|
|
3099
|
+
}
|
|
3100
|
+
return CHART_PALETTE[idx % CHART_PALETTE.length];
|
|
3101
|
+
}
|
|
3102
|
+
buildChartPlaceholder(label, element, shapeId) {
|
|
3103
|
+
const xfrm = this.xfrmFromFractions(element);
|
|
3104
|
+
const id = element.id ?? `chart-${shapeId}`;
|
|
3105
|
+
return '<p:sp><p:nvSpPr><p:cNvPr id="' + shapeId + '" name="' + Xml.attr(String(id)) + '"/><p:cNvSpPr/><p:nvPr/></p:nvSpPr><p:spPr>' + xfrm + '<a:prstGeom prst="roundRect"><a:avLst/></a:prstGeom><a:solidFill><a:srgbClr val="F1F5F9"/></a:solidFill><a:ln w="12700"><a:solidFill><a:srgbClr val="CBD5E1"/></a:solidFill></a:ln></p:spPr><p:txBody><a:bodyPr wrap="square" anchor="ctr" rtlCol="0"/><a:lstStyle/><a:p><a:pPr algn="ctr"/><a:r><a:rPr lang="en-US" sz="1600" b="1"><a:solidFill><a:srgbClr val="64748B"/></a:solidFill></a:rPr><a:t>' + Xml.text(label) + "</a:t></a:r></a:p></p:txBody></p:sp>";
|
|
3106
|
+
}
|
|
3107
|
+
buildPlaceholder(label, element, shapeId) {
|
|
3108
|
+
return this.buildTextShape(
|
|
3109
|
+
{
|
|
3110
|
+
id: element.id ?? `placeholder-${shapeId}`,
|
|
3111
|
+
type: "text",
|
|
3112
|
+
x: element.x ?? 0.1,
|
|
3113
|
+
y: element.y ?? 0.4,
|
|
3114
|
+
w: element.w ?? 0.8,
|
|
3115
|
+
h: element.h ?? 0.2,
|
|
3116
|
+
content: label,
|
|
3117
|
+
format: "plain",
|
|
3118
|
+
style: { fontSize: 20, align: "center", color: "#64748B" }
|
|
3119
|
+
},
|
|
3120
|
+
shapeId
|
|
3121
|
+
);
|
|
3122
|
+
}
|
|
3123
|
+
// ─── Text body / paragraphs / runs ────────────────────────────────────
|
|
3124
|
+
buildTextBody(content, style, format) {
|
|
3125
|
+
const fontPt = toFloat(style.fontSize ?? 24);
|
|
3126
|
+
const pt = Math.max(8, fontPt / 2);
|
|
3127
|
+
const sz = Emu.hundredthsOfPoint(pt);
|
|
3128
|
+
const baseBold = this.weightToBold(style.weight ?? null);
|
|
3129
|
+
const baseItalic = style.italic ? ' i="1"' : "";
|
|
3130
|
+
const baseUnderline = style.underline ? ' u="sng"' : "";
|
|
3131
|
+
const align = this.alignToAlgn(String(style.align ?? "left"));
|
|
3132
|
+
const [colorHex] = Color.parse(String(style.color ?? "#0F172A"), "0F172A");
|
|
3133
|
+
const fontFamily = style.fontFamily !== void 0 && style.fontFamily !== null ? '<a:latin typeface="' + Xml.attr(String(style.fontFamily)) + '"/>' : "";
|
|
3134
|
+
let anchor;
|
|
3135
|
+
switch (style.verticalAlign ?? "top") {
|
|
3136
|
+
case "middle":
|
|
3137
|
+
anchor = 't="ctr"';
|
|
3138
|
+
break;
|
|
3139
|
+
case "bottom":
|
|
3140
|
+
anchor = 't="b"';
|
|
3141
|
+
break;
|
|
3142
|
+
default:
|
|
3143
|
+
anchor = 't="t"';
|
|
3144
|
+
}
|
|
3145
|
+
const renderRuns = format === "markdown";
|
|
3146
|
+
let paragraphs = "";
|
|
3147
|
+
const lines = content.split("\n");
|
|
3148
|
+
for (const line of lines) {
|
|
3149
|
+
let headingLevel = 0;
|
|
3150
|
+
let isBullet = false;
|
|
3151
|
+
let body = line;
|
|
3152
|
+
if (renderRuns) {
|
|
3153
|
+
[headingLevel, body] = MarkdownInline.headingPrefix(line);
|
|
3154
|
+
if (headingLevel === 0) {
|
|
3155
|
+
[isBullet, body] = MarkdownInline.bulletPrefix(line);
|
|
3156
|
+
}
|
|
3157
|
+
}
|
|
3158
|
+
let paragraphSz = sz;
|
|
3159
|
+
let paragraphBold = baseBold;
|
|
3160
|
+
if (headingLevel > 0) {
|
|
3161
|
+
let multiplier;
|
|
3162
|
+
switch (headingLevel) {
|
|
3163
|
+
case 1:
|
|
3164
|
+
multiplier = 1.8;
|
|
3165
|
+
break;
|
|
3166
|
+
case 2:
|
|
3167
|
+
multiplier = 1.45;
|
|
3168
|
+
break;
|
|
3169
|
+
case 3:
|
|
3170
|
+
multiplier = 1.2;
|
|
3171
|
+
break;
|
|
3172
|
+
default:
|
|
3173
|
+
multiplier = 1;
|
|
3174
|
+
}
|
|
3175
|
+
paragraphSz = Emu.hundredthsOfPoint(pt * multiplier);
|
|
3176
|
+
paragraphBold = ' b="1"';
|
|
3177
|
+
}
|
|
3178
|
+
let pPr = '<a:pPr algn="' + align + '"';
|
|
3179
|
+
if (isBullet) {
|
|
3180
|
+
pPr += ' indent="-228600" marL="228600"><a:buFont typeface="Arial"/><a:buChar char="\u2022"/>';
|
|
3181
|
+
} else {
|
|
3182
|
+
pPr += "><a:buNone/>";
|
|
3183
|
+
}
|
|
3184
|
+
pPr += "</a:pPr>";
|
|
3185
|
+
let runs = "";
|
|
3186
|
+
if (renderRuns) {
|
|
3187
|
+
const tokens = MarkdownInline.tokenize(body);
|
|
3188
|
+
for (const token of tokens) {
|
|
3189
|
+
runs += this.buildRun(
|
|
3190
|
+
token.text,
|
|
3191
|
+
paragraphSz,
|
|
3192
|
+
paragraphBold,
|
|
3193
|
+
baseItalic,
|
|
3194
|
+
baseUnderline,
|
|
3195
|
+
colorHex,
|
|
3196
|
+
fontFamily,
|
|
3197
|
+
token.b,
|
|
3198
|
+
token.i,
|
|
3199
|
+
token.code
|
|
3200
|
+
);
|
|
3201
|
+
}
|
|
3202
|
+
} else {
|
|
3203
|
+
runs = this.buildRun(body, paragraphSz, paragraphBold, baseItalic, baseUnderline, colorHex, fontFamily, false, false, false);
|
|
3204
|
+
}
|
|
3205
|
+
paragraphs += "<a:p>" + pPr + runs + "</a:p>";
|
|
3206
|
+
}
|
|
3207
|
+
return '<p:txBody><a:bodyPr wrap="square" anchor="' + anchor.slice(3, -1) + '" rtlCol="0"/><a:lstStyle/>' + paragraphs + "</p:txBody>";
|
|
3208
|
+
}
|
|
3209
|
+
buildRun(text, sz, baseBold, baseItalic, baseUnderline, colorHex, fontFamily, bold, italic, code) {
|
|
3210
|
+
const b = bold ? ' b="1"' : baseBold;
|
|
3211
|
+
const i = (italic ? ' i="1"' : "") || baseItalic;
|
|
3212
|
+
const u = baseUnderline;
|
|
3213
|
+
let color = colorHex;
|
|
3214
|
+
let family = fontFamily;
|
|
3215
|
+
if (code) {
|
|
3216
|
+
color = "8B5CF6";
|
|
3217
|
+
family = '<a:latin typeface="Consolas"/>';
|
|
3218
|
+
}
|
|
3219
|
+
const rPr = '<a:rPr lang="en-US" sz="' + sz + '"' + b + i + u + '><a:solidFill><a:srgbClr val="' + color + '"/></a:solidFill>' + family + "</a:rPr>";
|
|
3220
|
+
return "<a:r>" + rPr + "<a:t>" + Xml.text(text) + "</a:t></a:r>";
|
|
3221
|
+
}
|
|
3222
|
+
weightToBold(weight) {
|
|
3223
|
+
if (isNumeric(weight) && toInt(weight) >= 600) {
|
|
3224
|
+
return ' b="1"';
|
|
3225
|
+
}
|
|
3226
|
+
if (weight === "bold" || weight === "semibold") {
|
|
3227
|
+
return ' b="1"';
|
|
3228
|
+
}
|
|
3229
|
+
return "";
|
|
3230
|
+
}
|
|
3231
|
+
alignToAlgn(align) {
|
|
3232
|
+
switch (align) {
|
|
3233
|
+
case "center":
|
|
3234
|
+
return "ctr";
|
|
3235
|
+
case "right":
|
|
3236
|
+
return "r";
|
|
3237
|
+
case "justify":
|
|
3238
|
+
return "just";
|
|
3239
|
+
default:
|
|
3240
|
+
return "l";
|
|
3241
|
+
}
|
|
3242
|
+
}
|
|
3243
|
+
// ─── Geometry helper ──────────────────────────────────────────────────
|
|
3244
|
+
xfrmFromFractions(element) {
|
|
3245
|
+
const x = Emu.fromFracX(toFloat(element.x ?? 0));
|
|
3246
|
+
const y = Emu.fromFracY(toFloat(element.y ?? 0));
|
|
3247
|
+
const cx = Emu.fromFracX(toFloat(element.w ?? 0));
|
|
3248
|
+
const cy = Emu.fromFracY(toFloat(element.h ?? 0));
|
|
3249
|
+
const rot = element.rotation !== void 0 ? Math.round(toFloat(element.rotation) * 6e4) : 0;
|
|
3250
|
+
const rotAttr = rot !== 0 ? ' rot="' + rot + '"' : "";
|
|
3251
|
+
return "<a:xfrm" + rotAttr + '><a:off x="' + x + '" y="' + y + '"/><a:ext cx="' + cx + '" cy="' + cy + '"/></a:xfrm>';
|
|
3252
|
+
}
|
|
3253
|
+
// ─── Media staging ────────────────────────────────────────────────────
|
|
3254
|
+
stageMedia(src, _slideNumber) {
|
|
3255
|
+
const data = this.loadImageBytes(src);
|
|
3256
|
+
if (data === null) {
|
|
3257
|
+
return null;
|
|
3258
|
+
}
|
|
3259
|
+
this.mediaCounter++;
|
|
3260
|
+
const i = this.mediaCounter;
|
|
3261
|
+
const ext = this.extensionForMime(data.mime) ?? "png";
|
|
3262
|
+
const archivePath = `ppt/media/image${i}.${ext}`;
|
|
3263
|
+
this.mediaFiles.push({ path: archivePath, bytes: data.bytes });
|
|
3264
|
+
const relId = "rId" + i;
|
|
3265
|
+
return {
|
|
3266
|
+
relId,
|
|
3267
|
+
target: `../media/image${i}.${ext}`,
|
|
3268
|
+
bytes: data.bytes
|
|
3269
|
+
};
|
|
3270
|
+
}
|
|
3271
|
+
loadImageBytes(src) {
|
|
3272
|
+
const m = /^data:([^;,]+)(?:;base64)?,([\s\S]*)$/.exec(src);
|
|
3273
|
+
if (m) {
|
|
3274
|
+
const mime = m[1];
|
|
3275
|
+
const payload = m[2];
|
|
3276
|
+
let bytes;
|
|
3277
|
+
if (src.includes(";base64,")) {
|
|
3278
|
+
try {
|
|
3279
|
+
bytes = base64Decode(payload);
|
|
3280
|
+
} catch {
|
|
3281
|
+
bytes = null;
|
|
3282
|
+
}
|
|
3283
|
+
} else {
|
|
3284
|
+
try {
|
|
3285
|
+
bytes = ENCODER.encode(decodeURIComponent(payload));
|
|
3286
|
+
} catch {
|
|
3287
|
+
bytes = null;
|
|
3288
|
+
}
|
|
3289
|
+
}
|
|
3290
|
+
if (bytes === null) {
|
|
3291
|
+
return null;
|
|
3292
|
+
}
|
|
3293
|
+
return { bytes, mime };
|
|
3294
|
+
}
|
|
3295
|
+
return null;
|
|
3296
|
+
}
|
|
3297
|
+
extensionForMime(mime) {
|
|
3298
|
+
switch (mime) {
|
|
3299
|
+
case "image/png":
|
|
3300
|
+
return "png";
|
|
3301
|
+
case "image/jpeg":
|
|
3302
|
+
return "jpg";
|
|
3303
|
+
case "image/gif":
|
|
3304
|
+
return "gif";
|
|
3305
|
+
case "image/svg+xml":
|
|
3306
|
+
return "svg";
|
|
3307
|
+
case "image/webp":
|
|
3308
|
+
return "webp";
|
|
3309
|
+
default:
|
|
3310
|
+
return null;
|
|
3311
|
+
}
|
|
3312
|
+
}
|
|
3313
|
+
// ─── Slide rels ───────────────────────────────────────────────────────
|
|
3314
|
+
buildSlideRels(slideNumber, hasNotes, layoutNumber = 1) {
|
|
3315
|
+
let rels = "";
|
|
3316
|
+
rels += '<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideLayout" Target="../slideLayouts/slideLayout' + layoutNumber + '.xml"/>';
|
|
3317
|
+
let nextRelNum = 2;
|
|
3318
|
+
if (hasNotes) {
|
|
3319
|
+
rels += '<Relationship Id="rId' + nextRelNum + '" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/notesSlide" Target="../notesSlides/notesSlide' + slideNumber + '.xml"/>';
|
|
3320
|
+
nextRelNum++;
|
|
3321
|
+
}
|
|
3322
|
+
for (const rel of this.pendingSlideRels[slideNumber] ?? []) {
|
|
3323
|
+
const mode = (rel.mode ?? null) === "External" ? ' TargetMode="External"' : "";
|
|
3324
|
+
rels += '<Relationship Id="' + Xml.attr(rel.id) + '" Type="' + Xml.attr(rel.type) + '" Target="' + Xml.attr(rel.target) + '"' + mode + "/>";
|
|
3325
|
+
}
|
|
3326
|
+
return Xml.declaration() + '<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">' + rels + "</Relationships>";
|
|
3327
|
+
}
|
|
3328
|
+
// ─── Notes slides ─────────────────────────────────────────────────────
|
|
3329
|
+
buildNotesSlideXml(slide, _slideNumber) {
|
|
3330
|
+
const notes = String(slide.notes ?? "");
|
|
3331
|
+
let paragraphs = "";
|
|
3332
|
+
for (const line of notes.split("\n")) {
|
|
3333
|
+
paragraphs += '<a:p><a:r><a:rPr lang="en-US" sz="1200"/><a:t>' + Xml.text(line) + "</a:t></a:r></a:p>";
|
|
3334
|
+
}
|
|
3335
|
+
return Xml.declaration() + '<p:notes xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"><p:cSld><p:spTree><p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr><p:grpSpPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="0" cy="0"/><a:chOff x="0" y="0"/><a:chExt cx="0" cy="0"/></a:xfrm></p:grpSpPr><p:sp><p:nvSpPr><p:cNvPr id="2" name="Notes Placeholder"/><p:cNvSpPr><a:spLocks noGrp="1"/></p:cNvSpPr><p:nvPr><p:ph type="body"/></p:nvPr></p:nvSpPr><p:spPr><a:xfrm><a:off x="685800" y="1700213"/><a:ext cx="5772150" cy="3679371"/></a:xfrm><a:prstGeom prst="rect"><a:avLst/></a:prstGeom></p:spPr><p:txBody><a:bodyPr/><a:lstStyle/>' + paragraphs + "</p:txBody></p:sp></p:spTree></p:cSld></p:notes>";
|
|
3336
|
+
}
|
|
3337
|
+
buildNotesSlideRels(slideNumber) {
|
|
3338
|
+
return Xml.declaration() + '<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide" Target="../slides/slide' + slideNumber + '.xml"/></Relationships>';
|
|
3339
|
+
}
|
|
3340
|
+
};
|
|
3341
|
+
function toFloat(v) {
|
|
3342
|
+
if (typeof v === "number") return v;
|
|
3343
|
+
const n = parseFloat(String(v));
|
|
3344
|
+
return Number.isFinite(n) ? n : 0;
|
|
3345
|
+
}
|
|
3346
|
+
function ucwords(s) {
|
|
3347
|
+
return s.replace(/(^|\s)([a-z])/g, (_m, sp, ch) => sp + ch.toUpperCase());
|
|
3348
|
+
}
|
|
3349
|
+
function innerXfrm(xfrm) {
|
|
3350
|
+
return xfrm.slice("<a:xfrm>".length, -"</a:xfrm>".length);
|
|
3351
|
+
}
|
|
3352
|
+
function isScalar2(v) {
|
|
3353
|
+
return typeof v === "string" || typeof v === "number" || typeof v === "boolean" || v === null || typeof v === "bigint";
|
|
3354
|
+
}
|
|
3355
|
+
function scalarToString(v) {
|
|
3356
|
+
if (v === true) return "1";
|
|
3357
|
+
if (v === false) return "";
|
|
3358
|
+
if (v === null) return "";
|
|
3359
|
+
return String(v);
|
|
3360
|
+
}
|
|
3361
|
+
function getImageSize(bytes) {
|
|
3362
|
+
if (bytes.length >= 24 && bytes[0] === 137 && bytes[1] === 80 && bytes[2] === 78 && bytes[3] === 71) {
|
|
3363
|
+
const w = bytes[16] << 24 | bytes[17] << 16 | bytes[18] << 8 | bytes[19];
|
|
3364
|
+
const h = bytes[20] << 24 | bytes[21] << 16 | bytes[22] << 8 | bytes[23];
|
|
3365
|
+
return [w >>> 0, h >>> 0];
|
|
3366
|
+
}
|
|
3367
|
+
if (bytes.length >= 10 && bytes[0] === 71 && bytes[1] === 73 && bytes[2] === 70) {
|
|
3368
|
+
const w = bytes[6] | bytes[7] << 8;
|
|
3369
|
+
const h = bytes[8] | bytes[9] << 8;
|
|
3370
|
+
return [w, h];
|
|
3371
|
+
}
|
|
3372
|
+
if (bytes.length >= 4 && bytes[0] === 255 && bytes[1] === 216) {
|
|
3373
|
+
let i = 2;
|
|
3374
|
+
while (i + 9 < bytes.length) {
|
|
3375
|
+
if (bytes[i] !== 255) {
|
|
3376
|
+
i++;
|
|
3377
|
+
continue;
|
|
3378
|
+
}
|
|
3379
|
+
const marker = bytes[i + 1];
|
|
3380
|
+
if (marker >= 192 && marker <= 207 && marker !== 196 && marker !== 200 && marker !== 204) {
|
|
3381
|
+
const h = bytes[i + 5] << 8 | bytes[i + 6];
|
|
3382
|
+
const w = bytes[i + 7] << 8 | bytes[i + 8];
|
|
3383
|
+
return [w, h];
|
|
3384
|
+
}
|
|
3385
|
+
const len = bytes[i + 2] << 8 | bytes[i + 3];
|
|
3386
|
+
i += 2 + len;
|
|
3387
|
+
}
|
|
3388
|
+
}
|
|
3389
|
+
return null;
|
|
3390
|
+
}
|
|
3391
|
+
|
|
3392
|
+
// src/agent.ts
|
|
3393
|
+
var VERSION = "0.5.2";
|
|
3394
|
+
function toU8(input) {
|
|
3395
|
+
return input instanceof Uint8Array ? input : new Uint8Array(input);
|
|
3396
|
+
}
|
|
3397
|
+
function makeWriter(options = {}) {
|
|
3398
|
+
return new PptxWriter(options.tempDir ?? null, options.allowHttpImages ?? false);
|
|
3399
|
+
}
|
|
3400
|
+
function assertValid(deck) {
|
|
3401
|
+
const errors = new Validator().validate(deck);
|
|
3402
|
+
if (errors.length > 0) {
|
|
3403
|
+
throw new SchemaException(
|
|
3404
|
+
"Deck failed schema validation. Call Agent::validateAndRepair() for a recoverable form.",
|
|
3405
|
+
errors
|
|
3406
|
+
);
|
|
3407
|
+
}
|
|
3408
|
+
}
|
|
3409
|
+
var Agent = {
|
|
3410
|
+
/** Validate a deck without writing. Empty array = valid. */
|
|
3411
|
+
validate(deck) {
|
|
3412
|
+
return new Validator().validate(deck);
|
|
3413
|
+
},
|
|
3414
|
+
/**
|
|
3415
|
+
* Validate + apply heuristic repairs. Returns `{ ok, schema, errors }` where
|
|
3416
|
+
* `ok` is true when the (possibly repaired) deck validates clean.
|
|
3417
|
+
*/
|
|
3418
|
+
validateAndRepair(deck) {
|
|
3419
|
+
const errors = this.validate(deck);
|
|
3420
|
+
if (errors.length === 0) {
|
|
3421
|
+
return { ok: true, schema: deck, errors: [] };
|
|
3422
|
+
}
|
|
3423
|
+
const repaired = new Repairer().repair(deck);
|
|
3424
|
+
const remaining = this.validate(repaired);
|
|
3425
|
+
return {
|
|
3426
|
+
ok: remaining.length === 0,
|
|
3427
|
+
schema: repaired,
|
|
3428
|
+
errors: remaining
|
|
3429
|
+
};
|
|
3430
|
+
},
|
|
3431
|
+
/** PPTX bytes for a deck (no temp file). Universal. Throws SchemaException if invalid. */
|
|
3432
|
+
toBytes(deck, options = {}) {
|
|
3433
|
+
assertValid(deck);
|
|
3434
|
+
return makeWriter(options).toBytes(deck);
|
|
3435
|
+
},
|
|
3436
|
+
/** Write a deck to disk as a PPTX file (Node only). Throws SchemaException if invalid. */
|
|
3437
|
+
async write(deck, path, options = {}) {
|
|
3438
|
+
assertValid(deck);
|
|
3439
|
+
const bytes = makeWriter(options).toBytes(deck);
|
|
3440
|
+
const fs = await import('fs');
|
|
3441
|
+
fs.writeFileSync(path, bytes);
|
|
3442
|
+
return { path, bytes: bytes.length, slides: deck?.slides?.length ?? 0 };
|
|
3443
|
+
},
|
|
3444
|
+
/** Read PPTX bytes back into the Deck schema. Universal. */
|
|
3445
|
+
read(input) {
|
|
3446
|
+
return new PptxReader().read(toU8(input));
|
|
3447
|
+
},
|
|
3448
|
+
/** Alias for {@see read}. */
|
|
3449
|
+
fromBytes(input) {
|
|
3450
|
+
return new PptxReader().read(toU8(input));
|
|
3451
|
+
},
|
|
3452
|
+
/** Plain-text summary of a deck. Mirrors PHP `Agent::describe`. */
|
|
3453
|
+
describe(deck) {
|
|
3454
|
+
const title = String(deck?.title ?? "Untitled");
|
|
3455
|
+
const themeName = String(deck?.theme?.name ?? Schema.DEFAULT_THEME_NAME);
|
|
3456
|
+
const slides = deck?.slides ?? [];
|
|
3457
|
+
const slideCount = slides.length;
|
|
3458
|
+
const elementCounts = {};
|
|
3459
|
+
for (const slide of slides) {
|
|
3460
|
+
for (const element of slide?.elements ?? []) {
|
|
3461
|
+
const type = String(element?.type ?? "unknown");
|
|
3462
|
+
elementCounts[type] = (elementCounts[type] ?? 0) + 1;
|
|
3463
|
+
}
|
|
3464
|
+
}
|
|
3465
|
+
const lines = [`Deck: ${title}`, `Theme: ${themeName}`, `Slides: ${slideCount}`];
|
|
3466
|
+
const keys = Object.keys(elementCounts);
|
|
3467
|
+
if (keys.length > 0) {
|
|
3468
|
+
const parts = keys.map((type) => `${elementCounts[type]} ${type}`);
|
|
3469
|
+
lines.push("Elements: " + parts.join(", "));
|
|
3470
|
+
}
|
|
3471
|
+
return lines.join("\n");
|
|
3472
|
+
},
|
|
3473
|
+
/** JSON Schema export for LLM tool-use registration. */
|
|
3474
|
+
jsonSchema() {
|
|
3475
|
+
return Schema.jsonSchema();
|
|
3476
|
+
},
|
|
3477
|
+
version() {
|
|
3478
|
+
return VERSION;
|
|
3479
|
+
}
|
|
3480
|
+
};
|
|
3481
|
+
|
|
3482
|
+
// src/dark-slide.ts
|
|
3483
|
+
var DarkSlide = class {
|
|
3484
|
+
constructor(tempDir = null, allowHttpImages = false) {
|
|
3485
|
+
this.tempDir = tempDir;
|
|
3486
|
+
this.allowHttpImages = allowHttpImages;
|
|
3487
|
+
}
|
|
3488
|
+
validate(deck) {
|
|
3489
|
+
return Agent.validate(deck);
|
|
3490
|
+
}
|
|
3491
|
+
validateAndRepair(deck) {
|
|
3492
|
+
return Agent.validateAndRepair(deck);
|
|
3493
|
+
}
|
|
3494
|
+
async write(deck, path) {
|
|
3495
|
+
return Agent.write(deck, path, { tempDir: this.tempDir ?? void 0, allowHttpImages: this.allowHttpImages });
|
|
3496
|
+
}
|
|
3497
|
+
toBytes(deck) {
|
|
3498
|
+
this.throwIfInvalid(deck);
|
|
3499
|
+
return new PptxWriter(this.tempDir, this.allowHttpImages).toBytes(deck);
|
|
3500
|
+
}
|
|
3501
|
+
throwIfInvalid(deck) {
|
|
3502
|
+
const errors = new Validator().validate(deck);
|
|
3503
|
+
if (errors.length > 0) {
|
|
3504
|
+
throw new SchemaException(
|
|
3505
|
+
"Deck failed schema validation. Call validateAndRepair() for a recoverable form.",
|
|
3506
|
+
errors
|
|
3507
|
+
);
|
|
3508
|
+
}
|
|
3509
|
+
}
|
|
3510
|
+
read(input) {
|
|
3511
|
+
return Agent.read(input);
|
|
3512
|
+
}
|
|
3513
|
+
describe(deck) {
|
|
3514
|
+
return Agent.describe(deck);
|
|
3515
|
+
}
|
|
3516
|
+
jsonSchema() {
|
|
3517
|
+
return Agent.jsonSchema();
|
|
3518
|
+
}
|
|
3519
|
+
};
|
|
3520
|
+
DarkSlide.VERSION = VERSION;
|
|
3521
|
+
|
|
3522
|
+
exports.Agent = Agent;
|
|
3523
|
+
exports.DarkSlide = DarkSlide;
|
|
3524
|
+
exports.PptxReader = PptxReader;
|
|
3525
|
+
exports.PptxWriter = PptxWriter;
|
|
3526
|
+
exports.Repairer = Repairer;
|
|
3527
|
+
exports.Schema = Schema;
|
|
3528
|
+
exports.SchemaException = SchemaException;
|
|
3529
|
+
exports.VERSION = VERSION;
|
|
3530
|
+
exports.Validator = Validator;
|
|
3531
|
+
exports.unzipSync = unzipSync;
|
|
3532
|
+
exports.zipSync = zipSync;
|
|
3533
|
+
//# sourceMappingURL=index.cjs.map
|
|
3534
|
+
//# sourceMappingURL=index.cjs.map
|