@pptx-studio/model 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,3042 @@
1
+ import { COLOR_TRANSFORM_OPS, MAPPED_COLOR_NAMES, PaintError, SCHEME_SLOTS, WHOLE_RECT, parseAngle, parsePercentage, resolveColor, transformShape } from "@pptx-studio/paint";
2
+ import { attributeValue, childElements, firstChild, parseXml, textContent } from "@pptx-studio/xml";
3
+ import { custGeom } from "@pptx-studio/geometry";
4
+ import { REL_TYPE } from "@pptx-studio/opc";
5
+ //#region src/errors.ts
6
+ var ModelError = class extends Error {
7
+ name = "ModelError";
8
+ code;
9
+ /** The part the failure is about, when there is one. */
10
+ partName;
11
+ /** Whatever detail names the failure: an attribute value, a rel id, a slot. */
12
+ detail;
13
+ constructor(code, message, partName = null, detail = null) {
14
+ super(message);
15
+ this.code = code;
16
+ this.partName = partName;
17
+ this.detail = detail;
18
+ }
19
+ };
20
+ function isModelError(value) {
21
+ return value instanceof ModelError;
22
+ }
23
+ const MODEL_ERROR_CODES = [
24
+ "MODEL_PART_MISSING",
25
+ "MODEL_PART_KIND",
26
+ "MODEL_NO_PRESENTATION",
27
+ "MODEL_NO_PARENT",
28
+ "MODEL_NO_THEME",
29
+ "MODEL_CLRMAP_INCOMPLETE",
30
+ "MODEL_CLRMAP_SLOT",
31
+ "MODEL_PLACEHOLDER_IDX",
32
+ "MODEL_XFRM_NUMBER",
33
+ "MODEL_GEOMETRY",
34
+ "MODEL_STYLE_IDX",
35
+ "MODEL_FONT_COLLECTION",
36
+ "MODEL_NO_STYLE_MATRIX",
37
+ "MODEL_STYLE_LIST_EMPTY",
38
+ "MODEL_SHEET_CYCLE",
39
+ "MODEL_FOREIGN_SHEET",
40
+ "MODEL_TEXT_ATTR",
41
+ "MODEL_TEXT_FIELD",
42
+ "MODEL_TEXT_LEVEL",
43
+ "MODEL_TEXT_TYPEFACE"
44
+ ];
45
+ //#endregion
46
+ //#region src/types.ts
47
+ const PLACEHOLDER_TYPES = [
48
+ "title",
49
+ "body",
50
+ "ctrTitle",
51
+ "subTitle",
52
+ "obj",
53
+ "chart",
54
+ "tbl",
55
+ "clipArt",
56
+ "dgm",
57
+ "media",
58
+ "sldImg",
59
+ "pic",
60
+ "sldNum",
61
+ "hdr",
62
+ "ftr",
63
+ "dt"
64
+ ];
65
+ /** The six a slide master may carry. Measured; see `PlaceholderType`. */
66
+ const MASTER_PLACEHOLDER_TYPES = [
67
+ "title",
68
+ "body",
69
+ "dt",
70
+ "ftr",
71
+ "sldNum",
72
+ "hdr"
73
+ ];
74
+ //#endregion
75
+ //#region src/parse/paint.ts
76
+ /**
77
+ * DrawingML markup into the types `@pptx-studio/paint` defines.
78
+ *
79
+ * Paint depends on nothing but arithmetic - it has never seen an `XElement` and
80
+ * must not, or it would inherit the XML layer's whole surface for the sake of
81
+ * six element names. So the crossing lives here, at layer 2, which is the first
82
+ * place that knows about both.
83
+ *
84
+ * Every function returns `undefined` when the element is absent, and `undefined`
85
+ * is load-bearing: "this shape declares no fill" and "this shape declares
86
+ * `a:noFill`" are different files with different resolutions, and only the first
87
+ * one inherits.
88
+ */
89
+ const COLOR_ELEMENTS = /* @__PURE__ */ new Set([
90
+ "a:srgbClr",
91
+ "a:scrgbClr",
92
+ "a:hslClr",
93
+ "a:schemeClr",
94
+ "a:sysClr",
95
+ "a:prstClr"
96
+ ]);
97
+ const FILL_ELEMENTS = /* @__PURE__ */ new Set([
98
+ "a:noFill",
99
+ "a:solidFill",
100
+ "a:gradFill",
101
+ "a:blipFill",
102
+ "a:pattFill",
103
+ "a:grpFill"
104
+ ]);
105
+ /** An integer attribute, or `fallback` when absent. Throws on a non-integer. */
106
+ function intAttr(element, name, fallback, what) {
107
+ const raw = attributeValue(element, name);
108
+ if (raw === void 0) return fallback;
109
+ const value = Number(raw);
110
+ if (!Number.isInteger(value)) throw new ModelError("MODEL_XFRM_NUMBER", `${what}/@${name} is "${raw}"`, null, raw);
111
+ return value;
112
+ }
113
+ /**
114
+ * `ST_Percentage`, which may legally be written `"40000"` or `"40%"`.
115
+ *
116
+ * The second spelling is Strict's and a Transitional part accepts it anyway -
117
+ * measured in 2.6. `parseInt` on `"40%"` yields 40, which is 0.04%, and that is
118
+ * how a line-spacing of 150% becomes a collapsed slide.
119
+ */
120
+ function percentAttr(element, name, fallback) {
121
+ const raw = attributeValue(element, name);
122
+ if (raw === void 0) return fallback;
123
+ return parsePercentage(raw);
124
+ }
125
+ function boolAttr(element, name, fallback) {
126
+ const raw = attributeValue(element, name);
127
+ if (raw === void 0) return fallback;
128
+ return raw === "1" || raw === "true";
129
+ }
130
+ function parseTransforms(element) {
131
+ const out = [];
132
+ for (const child of childElements(element)) {
133
+ const local = child.qname.startsWith("a:") ? child.qname.slice(2) : child.qname;
134
+ const shape = transformShape(local);
135
+ if (shape === null) continue;
136
+ const op = local;
137
+ const raw = attributeValue(child, "val");
138
+ if (shape === "flag") {
139
+ out.push({ op });
140
+ continue;
141
+ }
142
+ if (raw === void 0) throw new PaintError("COLOR_NUMBER", `a:${local} has no @val`, local);
143
+ const val = shape === "angle" ? parseAngle(raw) : parsePercentage(raw);
144
+ out.push({
145
+ op,
146
+ val
147
+ });
148
+ }
149
+ return out;
150
+ }
151
+ /** One of the six `EG_ColorChoice` members. */
152
+ function parseColorElement(element) {
153
+ const transforms = parseTransforms(element);
154
+ switch (element.qname) {
155
+ case "a:srgbClr": return {
156
+ space: "srgb",
157
+ hex: attributeValue(element, "val") ?? "",
158
+ transforms
159
+ };
160
+ case "a:scrgbClr": return {
161
+ space: "scrgb",
162
+ r: percentAttr(element, "r", 0),
163
+ g: percentAttr(element, "g", 0),
164
+ b: percentAttr(element, "b", 0),
165
+ transforms
166
+ };
167
+ case "a:hslClr": return {
168
+ space: "hsl",
169
+ hue: Number(attributeValue(element, "hue") ?? "0"),
170
+ sat: percentAttr(element, "sat", 0),
171
+ lum: percentAttr(element, "lum", 0),
172
+ transforms
173
+ };
174
+ case "a:schemeClr": return {
175
+ space: "scheme",
176
+ name: attributeValue(element, "val") ?? "",
177
+ transforms
178
+ };
179
+ case "a:sysClr": return {
180
+ space: "sys",
181
+ name: attributeValue(element, "val") ?? "",
182
+ lastClr: attributeValue(element, "lastClr") ?? null,
183
+ transforms
184
+ };
185
+ case "a:prstClr": return {
186
+ space: "prst",
187
+ name: attributeValue(element, "val") ?? "",
188
+ transforms
189
+ };
190
+ default: throw new PaintError("COLOR_SCHEME_NAME", `${element.qname} is not a colour element`, element.qname);
191
+ }
192
+ }
193
+ /** The first colour child of a wrapper such as `a:solidFill` or `a:fillRef`. */
194
+ function parseColorChild(parent) {
195
+ if (parent === void 0) return null;
196
+ for (const child of childElements(parent)) if (COLOR_ELEMENTS.has(child.qname)) return parseColorElement(child);
197
+ return null;
198
+ }
199
+ function parseRelativeRect(element) {
200
+ if (element === void 0) return WHOLE_RECT;
201
+ return {
202
+ l: percentAttr(element, "l", 0),
203
+ t: percentAttr(element, "t", 0),
204
+ r: percentAttr(element, "r", 0),
205
+ b: percentAttr(element, "b", 0)
206
+ };
207
+ }
208
+ function parseGradientShade(element) {
209
+ const lin = firstChild(element, "a:lin");
210
+ if (lin !== void 0) return {
211
+ kind: "linear",
212
+ ang: Number(attributeValue(lin, "ang") ?? "0"),
213
+ scaled: boolAttr(lin, "scaled", false)
214
+ };
215
+ const path = firstChild(element, "a:path");
216
+ if (path === void 0) return null;
217
+ const raw = attributeValue(path, "path") ?? "rect";
218
+ const kind = raw === "circle" || raw === "rect" ? raw : "shape";
219
+ const rect = firstChild(path, "a:fillToRect");
220
+ return {
221
+ kind: "path",
222
+ path: kind,
223
+ fillToRect: rect === void 0 ? null : parseRelativeRect(rect)
224
+ };
225
+ }
226
+ const TILE_ALIGNS = /* @__PURE__ */ new Set([
227
+ "tl",
228
+ "t",
229
+ "tr",
230
+ "l",
231
+ "ctr",
232
+ "r",
233
+ "bl",
234
+ "b",
235
+ "br"
236
+ ]);
237
+ const TILE_FLIPS = /* @__PURE__ */ new Set([
238
+ "none",
239
+ "x",
240
+ "y",
241
+ "xy"
242
+ ]);
243
+ /** `a:blip`'s colour effects, in document order, which is the order they apply. */
244
+ /** The `{96DAC541-...}` extension PowerPoint writes on a blip with an SVG original. */
245
+ const SVG_BLIP_EXT = "{96DAC541-7B7A-43D3-8B79-37D633B846F1}";
246
+ /** `a:blip/a:extLst/a:ext[@uri=SVG_BLIP_EXT]/asvg:svgBlip/@r:embed`, or `null`. */
247
+ function svgBlipEmbed(blip) {
248
+ const extLst = firstChild(blip, "a:extLst");
249
+ if (extLst === void 0) return null;
250
+ for (const ext of childElements(extLst)) {
251
+ if (ext.qname !== "a:ext" || attributeValue(ext, "uri") !== SVG_BLIP_EXT) continue;
252
+ for (const child of childElements(ext)) {
253
+ if (child.qname.split(":").pop() !== "svgBlip") continue;
254
+ const embed = attributeValue(child, "r:embed");
255
+ if (embed !== void 0) return embed;
256
+ }
257
+ }
258
+ return null;
259
+ }
260
+ function parseBlipEffects(blip) {
261
+ const effects = [];
262
+ for (const child of childElements(blip)) switch (child.qname) {
263
+ case "a:grayscl":
264
+ effects.push({ kind: "grayscale" });
265
+ break;
266
+ case "a:biLevel":
267
+ effects.push({
268
+ kind: "biLevel",
269
+ thresh: percentAttr(child, "thresh", 0)
270
+ });
271
+ break;
272
+ case "a:lum":
273
+ effects.push({
274
+ kind: "lum",
275
+ bright: percentAttr(child, "bright", 0),
276
+ contrast: percentAttr(child, "contrast", 0)
277
+ });
278
+ break;
279
+ case "a:duotone": {
280
+ const [from, to] = childElements(child).filter((c) => COLOR_ELEMENTS.has(c.qname)).map(parseColorElement);
281
+ if (from === void 0 || to === void 0) throw new ModelError("BLIP_DUOTONE", "a:duotone needs two colours", "a:duotone");
282
+ effects.push({
283
+ kind: "duotone",
284
+ from,
285
+ to
286
+ });
287
+ break;
288
+ }
289
+ case "a:alphaModFix":
290
+ effects.push({
291
+ kind: "alphaModFix",
292
+ amt: percentAttr(child, "amt", 1e5)
293
+ });
294
+ break;
295
+ case "a:clrChange": {
296
+ const from = parseColorChild(firstChild(child, "a:clrFrom"));
297
+ const to = parseColorChild(firstChild(child, "a:clrTo"));
298
+ if (from === null || to === null) throw new ModelError("BLIP_CLR_CHANGE", "a:clrChange needs both colours", "a:clrChange");
299
+ effects.push({
300
+ kind: "clrChange",
301
+ from,
302
+ to,
303
+ useAlpha: boolAttr(child, "useA", true)
304
+ });
305
+ break;
306
+ }
307
+ }
308
+ return effects;
309
+ }
310
+ /**
311
+ * `a:blipFill`.
312
+ *
313
+ * The relationship id is carried, never resolved: a fill in a layout resolves
314
+ * against the layout's rels and the same markup inherited onto a slide still
315
+ * means the layout's image. Same contract as `a:buBlip`.
316
+ */
317
+ function parseBlipFill(element, partName) {
318
+ const blip = firstChild(element, "a:blip");
319
+ if (blip === void 0) throw new ModelError("BLIP_NO_EMBED", "a:blipFill has no a:blip", "a:blipFill");
320
+ const embed = attributeValue(blip, "r:embed") ?? null;
321
+ const svgEmbed = svgBlipEmbed(blip);
322
+ if (embed === null && svgEmbed === null) throw new ModelError("BLIP_NO_EMBED", "a:blip names no image to resolve", "a:blip");
323
+ const tile = firstChild(element, "a:tile");
324
+ const stretch = firstChild(element, "a:stretch");
325
+ let mode;
326
+ if (tile !== void 0) {
327
+ const algn = attributeValue(tile, "algn") ?? "tl";
328
+ const flip = attributeValue(tile, "flip") ?? "none";
329
+ if (!TILE_ALIGNS.has(algn)) throw new ModelError("BLIP_TILE_ALIGN", `a:tile/@algn is "${algn}"`, "a:tile");
330
+ if (!TILE_FLIPS.has(flip)) throw new ModelError("BLIP_TILE_FLIP", `a:tile/@flip is "${flip}"`, "a:tile");
331
+ mode = {
332
+ kind: "tile",
333
+ tx: intAttr(tile, "tx", 0, "a:tile"),
334
+ ty: intAttr(tile, "ty", 0, "a:tile"),
335
+ sx: percentAttr(tile, "sx", 1e5),
336
+ sy: percentAttr(tile, "sy", 1e5),
337
+ flip,
338
+ algn
339
+ };
340
+ } else mode = {
341
+ kind: "stretch",
342
+ fillRect: parseRelativeRect(stretch === void 0 ? void 0 : firstChild(stretch, "a:fillRect"))
343
+ };
344
+ return {
345
+ type: "blip",
346
+ embed,
347
+ svgEmbed,
348
+ part: partName,
349
+ srcRect: parseRelativeRect(firstChild(element, "a:srcRect")),
350
+ mode,
351
+ effects: parseBlipEffects(blip),
352
+ dpi: intAttr(element, "dpi", 0, "a:blipFill"),
353
+ rotWithShape: boolAttr(element, "rotWithShape", true)
354
+ };
355
+ }
356
+ /**
357
+ * `p:pic/p:blipFill` - the image a picture shape draws.
358
+ *
359
+ * `CT_Picture` is `nvPicPr, blipFill, spPr`, so the image sits *beside*
360
+ * `p:spPr` rather than inside it, and in PresentationML. ADR 0037.
361
+ */
362
+ function parsePictureFill(pic, partName) {
363
+ const blipFill = firstChild(pic, "p:blipFill");
364
+ return blipFill === void 0 ? void 0 : parseBlipFill(blipFill, partName);
365
+ }
366
+ /** One member of `EG_FillProperties`. */
367
+ function parseFillElement(element, partName) {
368
+ switch (element.qname) {
369
+ case "a:noFill": return { type: "none" };
370
+ case "a:grpFill": return { type: "group" };
371
+ case "a:blipFill": return parseBlipFill(element, partName);
372
+ case "a:solidFill": {
373
+ const color = parseColorChild(element);
374
+ if (color === null) throw new PaintError("COLOR_SCHEME_NAME", "a:solidFill has no colour child", "a:solidFill");
375
+ return {
376
+ type: "solid",
377
+ color
378
+ };
379
+ }
380
+ case "a:pattFill": return {
381
+ type: "pattern",
382
+ prst: attributeValue(element, "prst") ?? "",
383
+ fg: parseColorChild(firstChild(element, "a:fgClr")),
384
+ bg: parseColorChild(firstChild(element, "a:bgClr"))
385
+ };
386
+ case "a:gradFill": {
387
+ const stops = [];
388
+ const list = firstChild(element, "a:gsLst");
389
+ if (list !== void 0) for (const gs of childElements(list)) {
390
+ if (gs.qname !== "a:gs") continue;
391
+ const color = parseColorChild(gs);
392
+ if (color === null) continue;
393
+ stops.push({
394
+ pos: percentAttr(gs, "pos", 0),
395
+ color
396
+ });
397
+ }
398
+ const flip = attributeValue(element, "flip") ?? "none";
399
+ return {
400
+ type: "gradient",
401
+ stops,
402
+ shade: parseGradientShade(element),
403
+ tileRect: firstChild(element, "a:tileRect") === void 0 ? null : parseRelativeRect(firstChild(element, "a:tileRect")),
404
+ flip,
405
+ rotWithShape: boolAttr(element, "rotWithShape", true)
406
+ };
407
+ }
408
+ default: throw new PaintError("FILL_PATTERN_UNKNOWN", `${element.qname} is not a fill element`, element.qname);
409
+ }
410
+ }
411
+ /** The fill a container declares, or `undefined` when it declares none. */
412
+ function parseFill(parent, partName) {
413
+ for (const child of childElements(parent)) if (FILL_ELEMENTS.has(child.qname)) return parseFillElement(child, partName);
414
+ }
415
+ const JOIN_KINDS = {
416
+ "a:round": "round",
417
+ "a:bevel": "bevel",
418
+ "a:miter": "miter"
419
+ };
420
+ function parseLineEnd(element) {
421
+ if (element === void 0) return null;
422
+ const size = (name) => {
423
+ const raw = attributeValue(element, name);
424
+ return raw === "sm" || raw === "med" || raw === "lg" ? raw : "med";
425
+ };
426
+ return {
427
+ type: attributeValue(element, "type") ?? "none",
428
+ w: size("w"),
429
+ len: size("len")
430
+ };
431
+ }
432
+ function parseDash(element) {
433
+ const preset = firstChild(element, "a:prstDash");
434
+ if (preset !== void 0) return {
435
+ kind: "preset",
436
+ val: attributeValue(preset, "val") ?? "solid"
437
+ };
438
+ const custom = firstChild(element, "a:custDash");
439
+ if (custom === void 0) return null;
440
+ const stops = [];
441
+ for (const ds of childElements(custom)) {
442
+ if (ds.qname !== "a:ds") continue;
443
+ stops.push({
444
+ d: percentAttr(ds, "d", 0) / 1e5,
445
+ sp: percentAttr(ds, "sp", 0) / 1e5
446
+ });
447
+ }
448
+ return {
449
+ kind: "custom",
450
+ stops
451
+ };
452
+ }
453
+ /** `a:ln`, lazily: every field is `null` when the attribute was absent. */
454
+ function parseLineElement(element, partName) {
455
+ let join = null;
456
+ for (const child of childElements(element)) {
457
+ const kind = JOIN_KINDS[child.qname];
458
+ if (kind === void 0) continue;
459
+ const lim = attributeValue(child, "lim");
460
+ join = {
461
+ kind,
462
+ limit: lim === void 0 ? null : parsePercentage(lim) / 1e5
463
+ };
464
+ break;
465
+ }
466
+ const cap = attributeValue(element, "cap");
467
+ const algn = attributeValue(element, "algn");
468
+ return {
469
+ w: attributeValue(element, "w") === void 0 ? null : intAttr(element, "w", 0, "a:ln"),
470
+ cap: cap === "flat" || cap === "sq" || cap === "rnd" ? cap : null,
471
+ cmpd: attributeValue(element, "cmpd") ?? null,
472
+ algn: algn === "ctr" || algn === "in" ? algn : null,
473
+ fill: parseFill(element, partName) ?? null,
474
+ dash: parseDash(element),
475
+ join,
476
+ headEnd: parseLineEnd(firstChild(element, "a:headEnd")),
477
+ tailEnd: parseLineEnd(firstChild(element, "a:tailEnd"))
478
+ };
479
+ }
480
+ function parseLine(parent, partName) {
481
+ const element = firstChild(parent, "a:ln");
482
+ return element === void 0 ? void 0 : parseLineElement(element, partName);
483
+ }
484
+ const RECT_ALIGNMENTS = /* @__PURE__ */ new Set([
485
+ "tl",
486
+ "t",
487
+ "tr",
488
+ "l",
489
+ "ctr",
490
+ "r",
491
+ "bl",
492
+ "b",
493
+ "br"
494
+ ]);
495
+ function alignAttr(element, fallback) {
496
+ const raw = attributeValue(element, "algn");
497
+ return raw !== void 0 && RECT_ALIGNMENTS.has(raw) ? raw : fallback;
498
+ }
499
+ function shadowGeometry(element) {
500
+ return {
501
+ blurRad: intAttr(element, "blurRad", 0, element.qname),
502
+ dist: intAttr(element, "dist", 0, element.qname),
503
+ dir: intAttr(element, "dir", 0, element.qname),
504
+ sx: percentAttr(element, "sx", 1e5),
505
+ sy: percentAttr(element, "sy", 1e5),
506
+ kx: intAttr(element, "kx", 0, element.qname),
507
+ ky: intAttr(element, "ky", 0, element.qname),
508
+ algn: alignAttr(element, "b"),
509
+ rotWithShape: boolAttr(element, "rotWithShape", true)
510
+ };
511
+ }
512
+ function requireColor(element) {
513
+ const color = parseColorChild(element);
514
+ if (color === null) throw new PaintError("COLOR_SCHEME_NAME", `${element.qname} has no colour child`, element.qname);
515
+ return color;
516
+ }
517
+ /** One member of `a:effectLst`, or `null` for an element this does not model. */
518
+ function parseEffectElement(element) {
519
+ switch (element.qname) {
520
+ case "a:outerShdw": return {
521
+ kind: "outerShdw",
522
+ color: requireColor(element),
523
+ ...shadowGeometry(element)
524
+ };
525
+ case "a:innerShdw": return {
526
+ kind: "innerShdw",
527
+ color: requireColor(element),
528
+ ...shadowGeometry(element)
529
+ };
530
+ case "a:prstShdw": return {
531
+ kind: "prstShdw",
532
+ prst: attributeValue(element, "prst") ?? "",
533
+ dist: intAttr(element, "dist", 0, "a:prstShdw"),
534
+ dir: intAttr(element, "dir", 0, "a:prstShdw"),
535
+ color: requireColor(element)
536
+ };
537
+ case "a:glow": return {
538
+ kind: "glow",
539
+ rad: intAttr(element, "rad", 0, "a:glow"),
540
+ color: requireColor(element)
541
+ };
542
+ case "a:softEdge": return {
543
+ kind: "softEdge",
544
+ rad: intAttr(element, "rad", 0, "a:softEdge")
545
+ };
546
+ case "a:blur": return {
547
+ kind: "blur",
548
+ rad: intAttr(element, "rad", 0, "a:blur"),
549
+ grow: boolAttr(element, "grow", true)
550
+ };
551
+ case "a:reflection": return {
552
+ kind: "reflection",
553
+ blurRad: intAttr(element, "blurRad", 0, "a:reflection"),
554
+ stA: percentAttr(element, "stA", 1e5),
555
+ stPos: percentAttr(element, "stPos", 0),
556
+ endA: percentAttr(element, "endA", 0),
557
+ endPos: percentAttr(element, "endPos", 1e5),
558
+ dist: intAttr(element, "dist", 0, "a:reflection"),
559
+ dir: intAttr(element, "dir", 0, "a:reflection"),
560
+ fadeDir: intAttr(element, "fadeDir", 54e5, "a:reflection"),
561
+ sx: percentAttr(element, "sx", 1e5),
562
+ sy: percentAttr(element, "sy", 1e5),
563
+ kx: intAttr(element, "kx", 0, "a:reflection"),
564
+ ky: intAttr(element, "ky", 0, "a:reflection"),
565
+ algn: alignAttr(element, "b"),
566
+ rotWithShape: boolAttr(element, "rotWithShape", true)
567
+ };
568
+ default: return null;
569
+ }
570
+ }
571
+ /**
572
+ * `a:effectLst`, in the order the file wrote it.
573
+ *
574
+ * Which is not the order it is painted in: 2.8 measured a glow painting *over*
575
+ * an outer shadow, the reverse of the schema's sequence. `effectFilter` in
576
+ * `paint` owns that, and this owns only the reading.
577
+ *
578
+ * `a:effectDag` is not modelled, so the shape is re-emitted byte for byte
579
+ * rather than approximated. PowerPoint writes none but does render one:
580
+ * measured on `a04-effects-03`, where it composites a glow over the fill.
581
+ */
582
+ function parseEffects(parent) {
583
+ const list = firstChild(parent, "a:effectLst");
584
+ if (list === void 0) return void 0;
585
+ const out = [];
586
+ for (const child of childElements(list)) {
587
+ const effect = parseEffectElement(child);
588
+ if (effect !== null) out.push(effect);
589
+ }
590
+ return out;
591
+ }
592
+ //#endregion
593
+ //#region src/parse/geometry.ts
594
+ /**
595
+ * `a:prstGeom` and `a:custGeom`, into the one type `@pptx-studio/geometry` takes.
596
+ *
597
+ * ## Why this is in the model rather than in the renderer
598
+ *
599
+ * Because geometry inherits. A slide placeholder that states no `a:spPr/a:xfrm`
600
+ * usually states no `a:prstGeom` either, and takes both from its layout - so
601
+ * "which shape is this" is a question for the resolver, and the resolver only
602
+ * works over fields on `Shape`. Reading the geometry off `shape.node` in a
603
+ * renderer would put half of one answer in the model and the other half
604
+ * somewhere that cannot see the chain.
605
+ *
606
+ * 2.9 recorded only the preset's *name*, which is enough to say what a shape is
607
+ * and not enough to draw it: `a:avLst` is where a rounded rectangle's corner
608
+ * radius and a chevron's notch depth live, and a preset drawn with its defaults
609
+ * when the file overrode them is a visibly wrong shape.
610
+ *
611
+ * ## The two are one type, and that is 2.4's finding rather than a convenience
612
+ *
613
+ * `a:custGeom` carries `avLst`, `gdLst`, `ahLst`, `cxnLst`, `rect` and
614
+ * `pathLst`: the same six children with the same meanings as a preset
615
+ * definition, written inline instead of looked up by name. So both parse into
616
+ * `Geometry` and the evaluator has one code path. The only difference is that a
617
+ * preset has a name and a `custGeom` does not.
618
+ */
619
+ function guides(parent, qname) {
620
+ const list = parent === void 0 ? void 0 : firstChild(parent, qname);
621
+ if (list === void 0) return [];
622
+ const out = [];
623
+ for (const child of childElements(list)) {
624
+ if (child.qname !== "a:gd") continue;
625
+ const name = attributeValue(child, "name");
626
+ const fmla = attributeValue(child, "fmla");
627
+ if (name === void 0 || fmla === void 0) continue;
628
+ out.push({
629
+ name,
630
+ fmla: fmla.trim().split(/\s+/)
631
+ });
632
+ }
633
+ return out;
634
+ }
635
+ function point(element, partName) {
636
+ const x = attributeValue(element, "x");
637
+ const y = attributeValue(element, "y");
638
+ if (x === void 0 || y === void 0) throw new ModelError("MODEL_GEOMETRY", `<${element.qname}> has no x or no y`, partName);
639
+ return {
640
+ x,
641
+ y
642
+ };
643
+ }
644
+ const PATH_FILLS = /* @__PURE__ */ new Set([
645
+ "none",
646
+ "norm",
647
+ "lighten",
648
+ "lightenLess",
649
+ "darken",
650
+ "darkenLess"
651
+ ]);
652
+ function pathFill(raw) {
653
+ return raw !== void 0 && PATH_FILLS.has(raw) ? raw : "norm";
654
+ }
655
+ /** `@w`/`@h` on an `a:path`. Absent means zero, which means no path space. */
656
+ function pathExtent(element, name, partName) {
657
+ const raw = attributeValue(element, name);
658
+ if (raw === void 0) return 0;
659
+ const value = Number(raw);
660
+ if (!Number.isFinite(value)) throw new ModelError("MODEL_GEOMETRY", `a:path/@${name} is "${raw}"`, partName);
661
+ return value;
662
+ }
663
+ function commands(path, partName) {
664
+ const out = [];
665
+ for (const child of childElements(path)) switch (child.qname) {
666
+ case "a:moveTo":
667
+ case "a:lnTo": {
668
+ const pt = firstChild(child, "a:pt");
669
+ if (pt === void 0) break;
670
+ out.push({
671
+ kind: child.qname === "a:moveTo" ? "moveTo" : "lnTo",
672
+ to: point(pt, partName)
673
+ });
674
+ break;
675
+ }
676
+ case "a:quadBezTo": {
677
+ const pts = childElements(child).filter((c) => c.qname === "a:pt");
678
+ if (pts.length < 2) break;
679
+ out.push({
680
+ kind: "quadBezTo",
681
+ c1: point(pts[0], partName),
682
+ to: point(pts[1], partName)
683
+ });
684
+ break;
685
+ }
686
+ case "a:cubicBezTo": {
687
+ const pts = childElements(child).filter((c) => c.qname === "a:pt");
688
+ if (pts.length < 3) break;
689
+ out.push({
690
+ kind: "cubicBezTo",
691
+ c1: point(pts[0], partName),
692
+ c2: point(pts[1], partName),
693
+ to: point(pts[2], partName)
694
+ });
695
+ break;
696
+ }
697
+ case "a:arcTo": {
698
+ const wR = attributeValue(child, "wR");
699
+ const hR = attributeValue(child, "hR");
700
+ const stAng = attributeValue(child, "stAng");
701
+ const swAng = attributeValue(child, "swAng");
702
+ if (wR === void 0 || hR === void 0 || stAng === void 0 || swAng === void 0) throw new ModelError("MODEL_GEOMETRY", "a:arcTo is missing an attribute", partName);
703
+ out.push({
704
+ kind: "arcTo",
705
+ wR,
706
+ hR,
707
+ stAng,
708
+ swAng
709
+ });
710
+ break;
711
+ }
712
+ case "a:close": out.push({ kind: "close" });
713
+ }
714
+ return out;
715
+ }
716
+ function paths(parent, partName) {
717
+ const list = firstChild(parent, "a:pathLst");
718
+ if (list === void 0) return [];
719
+ const out = [];
720
+ for (const child of childElements(list)) {
721
+ if (child.qname !== "a:path") continue;
722
+ out.push({
723
+ w: pathExtent(child, "w", partName),
724
+ h: pathExtent(child, "h", partName),
725
+ fill: pathFill(attributeValue(child, "fill")),
726
+ stroke: attributeValue(child, "stroke") !== "0",
727
+ extrusionOk: attributeValue(child, "extrusionOk") === "1",
728
+ commands: commands(child, partName)
729
+ });
730
+ }
731
+ return out;
732
+ }
733
+ function handles(parent, partName) {
734
+ const list = firstChild(parent, "a:ahLst");
735
+ if (list === void 0) return [];
736
+ const out = [];
737
+ for (const child of childElements(list)) {
738
+ const pos = firstChild(child, "a:pos");
739
+ if (pos === void 0) continue;
740
+ const at = point(pos, partName);
741
+ if (child.qname === "a:ahXY") out.push({
742
+ kind: "xy",
743
+ pos: at,
744
+ gdRefX: attributeValue(child, "gdRefX") ?? null,
745
+ minX: attributeValue(child, "minX") ?? null,
746
+ maxX: attributeValue(child, "maxX") ?? null,
747
+ gdRefY: attributeValue(child, "gdRefY") ?? null,
748
+ minY: attributeValue(child, "minY") ?? null,
749
+ maxY: attributeValue(child, "maxY") ?? null
750
+ });
751
+ else if (child.qname === "a:ahPolar") out.push({
752
+ kind: "polar",
753
+ pos: at,
754
+ gdRefAng: attributeValue(child, "gdRefAng") ?? null,
755
+ minAng: attributeValue(child, "minAng") ?? null,
756
+ maxAng: attributeValue(child, "maxAng") ?? null,
757
+ gdRefR: attributeValue(child, "gdRefR") ?? null,
758
+ minR: attributeValue(child, "minR") ?? null,
759
+ maxR: attributeValue(child, "maxR") ?? null
760
+ });
761
+ }
762
+ return out;
763
+ }
764
+ function sites(parent, partName) {
765
+ const list = firstChild(parent, "a:cxnLst");
766
+ if (list === void 0) return [];
767
+ const out = [];
768
+ for (const child of childElements(list)) {
769
+ if (child.qname !== "a:cxn") continue;
770
+ const pos = firstChild(child, "a:pos");
771
+ const ang = attributeValue(child, "ang");
772
+ if (pos === void 0 || ang === void 0) continue;
773
+ out.push({
774
+ ang,
775
+ pos: point(pos, partName)
776
+ });
777
+ }
778
+ return out;
779
+ }
780
+ function textRect(parent) {
781
+ const rect = firstChild(parent, "a:rect");
782
+ if (rect === void 0) return null;
783
+ const l = attributeValue(rect, "l");
784
+ const t = attributeValue(rect, "t");
785
+ const r = attributeValue(rect, "r");
786
+ const b = attributeValue(rect, "b");
787
+ return l === void 0 || t === void 0 || r === void 0 || b === void 0 ? null : {
788
+ l,
789
+ t,
790
+ r,
791
+ b
792
+ };
793
+ }
794
+ /**
795
+ * The geometry a shape declares, or `undefined` when it declares none.
796
+ *
797
+ * `undefined` is the whole point: a slide placeholder with no `a:prstGeom` has
798
+ * not chosen to be a rectangle, it has said nothing, and `resolve` is what turns
799
+ * that into the layout's answer.
800
+ */
801
+ function parseGeometry(spPr, partName) {
802
+ if (spPr === void 0) return void 0;
803
+ const preset = firstChild(spPr, "a:prstGeom");
804
+ if (preset !== void 0) {
805
+ const prst = attributeValue(preset, "prst");
806
+ if (prst === void 0) throw new ModelError("MODEL_GEOMETRY", "a:prstGeom has no @prst", partName);
807
+ return {
808
+ kind: "preset",
809
+ prst,
810
+ adjust: guides(preset, "a:avLst")
811
+ };
812
+ }
813
+ const custom = firstChild(spPr, "a:custGeom");
814
+ if (custom === void 0) return void 0;
815
+ return {
816
+ kind: "custom",
817
+ geometry: custGeom({
818
+ avLst: guides(custom, "a:avLst"),
819
+ gdLst: guides(custom, "a:gdLst"),
820
+ ahLst: handles(custom, partName),
821
+ cxnLst: sites(custom, partName),
822
+ rect: textRect(custom),
823
+ pathLst: paths(custom, partName)
824
+ })
825
+ };
826
+ }
827
+ //#endregion
828
+ //#region src/parse/body.ts
829
+ /**
830
+ * `a:bodyPr`: the text frame, as the file states it.
831
+ *
832
+ * Every field is `T | undefined` for the reason the rest of the model is - a
833
+ * frame that states no anchor is not a top-anchored frame, and 3.6 measured that
834
+ * the difference is the whole of the placeholder cascade.
835
+ */
836
+ const ANCHORS = [
837
+ "t",
838
+ "ctr",
839
+ "b",
840
+ "just",
841
+ "dist"
842
+ ];
843
+ const VERTICALS = [
844
+ "horz",
845
+ "vert",
846
+ "vert270",
847
+ "wordArtVert",
848
+ "eaVert",
849
+ "mongolianVert",
850
+ "wordArtVertRtl"
851
+ ];
852
+ const WRAPS = ["none", "square"];
853
+ const VERT_OVERFLOWS = [
854
+ "overflow",
855
+ "ellipsis",
856
+ "clip"
857
+ ];
858
+ const HORZ_OVERFLOWS = ["overflow", "clip"];
859
+ function attrError(element, name, raw, why, part) {
860
+ throw new ModelError("MODEL_TEXT_ATTR", `${element.qname}/@${name} is "${raw}", which is not ${why}`, part, name);
861
+ }
862
+ function boolOf$1(element, name, part) {
863
+ const raw = attributeValue(element, name);
864
+ if (raw === void 0) return void 0;
865
+ if (raw === "1" || raw === "true") return true;
866
+ if (raw === "0" || raw === "false") return false;
867
+ return attrError(element, name, raw, "a ST_OnOff", part);
868
+ }
869
+ function intOf$2(element, name, part) {
870
+ const raw = attributeValue(element, name);
871
+ if (raw === void 0) return void 0;
872
+ const value = Number(raw);
873
+ if (!Number.isInteger(value)) return attrError(element, name, raw, "an integer", part);
874
+ return value;
875
+ }
876
+ function enumOf$1(element, name, allowed, part) {
877
+ const raw = attributeValue(element, name);
878
+ if (raw === void 0) return void 0;
879
+ if (!allowed.includes(raw)) return attrError(element, name, raw, `one of ${allowed.join(", ")}`, part);
880
+ return raw;
881
+ }
882
+ /**
883
+ * `a:noAutofit` / `a:normAutofit` / `a:spAutoFit`, whichever is present.
884
+ *
885
+ * The scale attributes are `ST_TextFontScalePercentOrPercentString`, which is
886
+ * the same both-spellings grammar as every other percentage here; they are kept
887
+ * as thousandths so `@pptx-studio/text` reads them the way it was measured.
888
+ */
889
+ function parseAutofit(element, part) {
890
+ if (firstChild(element, "a:noAutofit") !== void 0) return { kind: "none" };
891
+ if (firstChild(element, "a:spAutoFit") !== void 0) return { kind: "shape" };
892
+ const norm = firstChild(element, "a:normAutofit");
893
+ if (norm === void 0) return void 0;
894
+ return {
895
+ kind: "normal",
896
+ fontScale: percentOf(norm, "fontScale", part),
897
+ lnSpcReduction: percentOf(norm, "lnSpcReduction", part)
898
+ };
899
+ }
900
+ /** Thousandths of a percent, accepting both the `92500` and the `92.5%` spellings. */
901
+ function percentOf(element, name, part) {
902
+ const raw = attributeValue(element, name);
903
+ if (raw === void 0) return void 0;
904
+ const value = raw.endsWith("%") ? Number(raw.slice(0, -1)) * 1e3 : Number(raw);
905
+ if (!Number.isFinite(value)) return attrError(element, name, raw, "a ST_Percentage", part);
906
+ return value;
907
+ }
908
+ /** Parse an `a:bodyPr`. Lengths stay in EMU and angles in 60000ths of a degree. */
909
+ function parseBodyProps(element, partName) {
910
+ return {
911
+ anchor: enumOf$1(element, "anchor", ANCHORS, partName),
912
+ anchorCtr: boolOf$1(element, "anchorCtr", partName),
913
+ lIns: intOf$2(element, "lIns", partName),
914
+ tIns: intOf$2(element, "tIns", partName),
915
+ rIns: intOf$2(element, "rIns", partName),
916
+ bIns: intOf$2(element, "bIns", partName),
917
+ vert: enumOf$1(element, "vert", VERTICALS, partName),
918
+ wrap: enumOf$1(element, "wrap", WRAPS, partName),
919
+ vertOverflow: enumOf$1(element, "vertOverflow", VERT_OVERFLOWS, partName),
920
+ horzOverflow: enumOf$1(element, "horzOverflow", HORZ_OVERFLOWS, partName),
921
+ rot: intOf$2(element, "rot", partName),
922
+ upright: boolOf$1(element, "upright", partName),
923
+ numCol: intOf$2(element, "numCol", partName),
924
+ spcCol: intOf$2(element, "spcCol", partName),
925
+ rtlCol: boolOf$1(element, "rtlCol", partName),
926
+ spcFirstLastPara: boolOf$1(element, "spcFirstLastPara", partName),
927
+ compatLnSpc: boolOf$1(element, "compatLnSpc", partName),
928
+ fromWordArt: boolOf$1(element, "fromWordArt", partName),
929
+ forceAA: boolOf$1(element, "forceAA", partName),
930
+ autofit: parseAutofit(element, partName),
931
+ node: element
932
+ };
933
+ }
934
+ /** The `a:bodyPr` of a `p:txBody`, when it has one. */
935
+ function parseBodyPropsChild(parent, partName) {
936
+ const element = firstChild(parent, "a:bodyPr");
937
+ return element === void 0 ? void 0 : parseBodyProps(element, partName);
938
+ }
939
+ //#endregion
940
+ //#region src/text.ts
941
+ const THEME_FONT = /^\+(mj|mn)-(lt|ea|cs)$/;
942
+ /** `+mn-lt` and its five siblings, or `null` for a real typeface name. */
943
+ function themeFontRef(typeface) {
944
+ const match = THEME_FONT.exec(typeface);
945
+ if (match === null) return null;
946
+ const script = match[2];
947
+ return {
948
+ collection: match[1] === "mj" ? "major" : "minor",
949
+ script: script === "lt" ? "latin" : script
950
+ };
951
+ }
952
+ /** The number of levels a `CT_TextListStyle` holds, and `@lvl`'s range plus one. */
953
+ const LEVELS = 9;
954
+ //#endregion
955
+ //#region src/parse/text.ts
956
+ /**
957
+ * `p:txBody`, `a:lstStyle` and `p:txStyles` into the text model.
958
+ *
959
+ * Nothing here resolves anything and nothing here defaults anything the file
960
+ * could have said. That is the same rule the rest of the parser follows, and in
961
+ * this one corner it is load-bearing rather than tidy: `@marL` and `@indent`
962
+ * have schema defaults of 347663 and -342900, and a parser that materialises
963
+ * them gives every paragraph a 27-point hanging indent it never asked for *and*
964
+ * blocks the value it should have inherited, because a default that has been
965
+ * written down cannot be told from a declaration. Measured in 3.1 - PowerPoint
966
+ * resolves both to 0 when nothing declares them.
967
+ *
968
+ * The one place a value is supplied is `Paragraph.level`, which is 0 when
969
+ * `@lvl` is absent because that is what the attribute means rather than what the
970
+ * schema says, and which is not inherited by anything.
971
+ */
972
+ /**
973
+ * `ST_OnOff`, which has four lexical forms and not two.
974
+ *
975
+ * `1`/`true` and `0`/`false`. Anything else is a file that says something we do
976
+ * not understand, and guessing is how an explicit `b="0"` becomes bold.
977
+ */
978
+ function boolOf(element, name, partName) {
979
+ const raw = attributeValue(element, name);
980
+ if (raw === void 0) return void 0;
981
+ if (raw === "1" || raw === "true") return true;
982
+ if (raw === "0" || raw === "false") return false;
983
+ throw new ModelError("MODEL_TEXT_ATTR", `${element.qname}/@${name} is "${raw}", which is not a ST_OnOff`, partName, name);
984
+ }
985
+ function intOf$1(element, name, partName) {
986
+ const raw = attributeValue(element, name);
987
+ if (raw === void 0) return void 0;
988
+ const value = Number(raw);
989
+ if (!Number.isInteger(value)) throw new ModelError("MODEL_TEXT_ATTR", `${element.qname}/@${name} is "${raw}", which is not an integer`, partName, name);
990
+ return value;
991
+ }
992
+ /**
993
+ * An attribute whose value must be one of a fixed set.
994
+ *
995
+ * A value outside the set throws rather than falling back to the first member.
996
+ * A silent fallback here paints the wrong thing and gives nothing to trace it
997
+ * by, and `ST_TextAlignType` in particular has a member - `just` - whose
998
+ * substitute would be plausible on most slides and wrong on the rest.
999
+ */
1000
+ function enumOf(element, name, allowed, partName) {
1001
+ const raw = attributeValue(element, name);
1002
+ if (raw === void 0) return void 0;
1003
+ if (!allowed.includes(raw)) throw new ModelError("MODEL_TEXT_ATTR", `${element.qname}/@${name} is "${raw}", which is not one of ${allowed.join(", ")}`, partName, name);
1004
+ return raw;
1005
+ }
1006
+ const ALIGN = [
1007
+ "l",
1008
+ "ctr",
1009
+ "r",
1010
+ "just",
1011
+ "justLow",
1012
+ "dist",
1013
+ "thaiDist"
1014
+ ];
1015
+ const FONT_ALIGN = [
1016
+ "auto",
1017
+ "t",
1018
+ "ctr",
1019
+ "base",
1020
+ "b"
1021
+ ];
1022
+ const STRIKE = [
1023
+ "noStrike",
1024
+ "sngStrike",
1025
+ "dblStrike"
1026
+ ];
1027
+ const CAPS = [
1028
+ "none",
1029
+ "small",
1030
+ "all"
1031
+ ];
1032
+ const UNDERLINE = [
1033
+ "none",
1034
+ "words",
1035
+ "sng",
1036
+ "dbl",
1037
+ "heavy",
1038
+ "dotted",
1039
+ "dottedHeavy",
1040
+ "dash",
1041
+ "dashHeavy",
1042
+ "dashLong",
1043
+ "dashLongHeavy",
1044
+ "dotDash",
1045
+ "dotDashHeavy",
1046
+ "dotDotDash",
1047
+ "dotDotDashHeavy",
1048
+ "wavy",
1049
+ "wavyHeavy",
1050
+ "wavyDbl"
1051
+ ];
1052
+ /**
1053
+ * `a:latin` and its three siblings.
1054
+ *
1055
+ * `@charset` is **signed**: Shift-JIS is written `-128`, and reading it as an
1056
+ * unsigned byte turns the one value that identifies Japanese text into 128,
1057
+ * which is nothing. `@panose` is kept as the twenty hex characters it is,
1058
+ * because it is written back verbatim and nothing here interprets it.
1059
+ */
1060
+ function typefaceOf(element, partName) {
1061
+ const typeface = attributeValue(element, "typeface");
1062
+ if (typeface === void 0) throw new ModelError("MODEL_TEXT_ATTR", `${element.qname} has no @typeface`, partName, element.qname);
1063
+ return {
1064
+ typeface,
1065
+ panose: attributeValue(element, "panose"),
1066
+ pitchFamily: intOf$1(element, "pitchFamily", partName),
1067
+ charset: intOf$1(element, "charset", partName)
1068
+ };
1069
+ }
1070
+ /**
1071
+ * A named typeface child.
1072
+ *
1073
+ * Split from `typefaceOf` because `a:buFont` carries the same attributes on the
1074
+ * element itself rather than on a child of a known name, and reading it through
1075
+ * a lookup by qname would need the caller to pass its own element as its own
1076
+ * parent.
1077
+ */
1078
+ function parseTypeface(parent, qname, partName) {
1079
+ const element = firstChild(parent, qname);
1080
+ return element === void 0 ? void 0 : typefaceOf(element, partName);
1081
+ }
1082
+ /** `a:rPr`, `a:defRPr` or `a:endParaRPr` - one type under three names. */
1083
+ function parseRunProps(element, partName) {
1084
+ return {
1085
+ sz: intOf$1(element, "sz", partName),
1086
+ b: boolOf(element, "b", partName),
1087
+ i: boolOf(element, "i", partName),
1088
+ u: enumOf(element, "u", UNDERLINE, partName),
1089
+ strike: enumOf(element, "strike", STRIKE, partName),
1090
+ cap: enumOf(element, "cap", CAPS, partName),
1091
+ spc: intOf$1(element, "spc", partName),
1092
+ kern: intOf$1(element, "kern", partName),
1093
+ baseline: intOf$1(element, "baseline", partName),
1094
+ noProof: boolOf(element, "noProof", partName),
1095
+ lang: attributeValue(element, "lang"),
1096
+ altLang: attributeValue(element, "altLang"),
1097
+ latin: parseTypeface(element, "a:latin", partName),
1098
+ ea: parseTypeface(element, "a:ea", partName),
1099
+ cs: parseTypeface(element, "a:cs", partName),
1100
+ sym: parseTypeface(element, "a:sym", partName),
1101
+ fill: parseFill(element, partName),
1102
+ line: parseLine(element, partName),
1103
+ effects: parseEffects(element),
1104
+ highlight: parseColorChild(firstChild(element, "a:highlight")) ?? void 0,
1105
+ node: element
1106
+ };
1107
+ }
1108
+ function parseRunPropsChild(parent, qname, partName) {
1109
+ const element = firstChild(parent, qname);
1110
+ return element === void 0 ? void 0 : parseRunProps(element, partName);
1111
+ }
1112
+ /**
1113
+ * `a:lnSpc`, `a:spcBef` or `a:spcAft`.
1114
+ *
1115
+ * The child's name is the unit. `a:spcPct` is a `ST_TextSpacingPercentOrPercentString`
1116
+ * and so accepts both the `150000` and the `150%` spellings - `parsePercentage`
1117
+ * handles the second, measured in 2.6 for colour and again in 3.1 here.
1118
+ * `a:spcPts` is hundredths of a point and has only one spelling.
1119
+ */
1120
+ function parseSpacing(parent, qname, partName) {
1121
+ const element = firstChild(parent, qname);
1122
+ if (element === void 0) return void 0;
1123
+ const pct = firstChild(element, "a:spcPct");
1124
+ if (pct !== void 0) {
1125
+ const raw = attributeValue(pct, "val");
1126
+ if (raw === void 0) throw new ModelError("MODEL_TEXT_ATTR", `${qname}/a:spcPct has no @val`, partName, qname);
1127
+ return {
1128
+ kind: "percent",
1129
+ value: parsePercentage(raw)
1130
+ };
1131
+ }
1132
+ const pts = firstChild(element, "a:spcPts");
1133
+ if (pts !== void 0) {
1134
+ const value = intOf$1(pts, "val", partName);
1135
+ if (value === void 0) throw new ModelError("MODEL_TEXT_ATTR", `${qname}/a:spcPts has no @val`, partName, qname);
1136
+ return {
1137
+ kind: "points",
1138
+ value
1139
+ };
1140
+ }
1141
+ }
1142
+ /** `a:pPr`, `a:defPPr` or any `a:lvlNpPr` - one type under eleven names. */
1143
+ function parseParaProps(element, partName) {
1144
+ return {
1145
+ marL: intOf$1(element, "marL", partName),
1146
+ marR: intOf$1(element, "marR", partName),
1147
+ indent: intOf$1(element, "indent", partName),
1148
+ algn: enumOf(element, "algn", ALIGN, partName),
1149
+ defTabSz: intOf$1(element, "defTabSz", partName),
1150
+ rtl: boolOf(element, "rtl", partName),
1151
+ eaLnBrk: boolOf(element, "eaLnBrk", partName),
1152
+ fontAlgn: enumOf(element, "fontAlgn", FONT_ALIGN, partName),
1153
+ latinLnBrk: boolOf(element, "latinLnBrk", partName),
1154
+ hangingPunct: boolOf(element, "hangingPunct", partName),
1155
+ lnSpc: parseSpacing(element, "a:lnSpc", partName),
1156
+ spcBef: parseSpacing(element, "a:spcBef", partName),
1157
+ spcAft: parseSpacing(element, "a:spcAft", partName),
1158
+ defRPr: parseRunPropsChild(element, "a:defRPr", partName),
1159
+ ...parseBullet(element, partName),
1160
+ node: element
1161
+ };
1162
+ }
1163
+ /** The four bullet elements, of which at most one may appear. */
1164
+ const BULLET_KINDS = [
1165
+ ["a:buNone", "none"],
1166
+ ["a:buChar", "char"],
1167
+ ["a:buAutoNum", "autonum"],
1168
+ ["a:buBlip", "blip"]
1169
+ ];
1170
+ /**
1171
+ * The bullet, as four independent slots plus the exclusive kind.
1172
+ *
1173
+ * Four rather than one because T5 measured them to merge separately: a level
1174
+ * that declares only `a:buFont` re-faces an inherited `a:buChar` and keeps
1175
+ * everything else. Folding them into a single value would make that impossible
1176
+ * to express, and the fold is what a reader writes first because the schema puts
1177
+ * the four kinds in one exclusive group.
1178
+ *
1179
+ * `a:buFontTx`, `a:buSzTx` and `a:buClrTx` are parsed as *values* rather than as
1180
+ * absences, because they are: PowerPoint writes nothing at all to mean "follow
1181
+ * the text", so the explicit element only ever appears in order to cancel
1182
+ * something a level above declared.
1183
+ */
1184
+ function parseBullet(element, partName) {
1185
+ let buKind;
1186
+ for (const [name, kind] of BULLET_KINDS) if (firstChild(element, name) !== void 0) {
1187
+ if (buKind !== void 0) throw new ModelError("MODEL_TEXT_BULLET", `a:pPr declares both ${buKind} and ${kind} bullets, which are exclusive`, partName, "a:pPr");
1188
+ buKind = kind;
1189
+ }
1190
+ const charElement = firstChild(element, "a:buChar");
1191
+ const autoNum = firstChild(element, "a:buAutoNum");
1192
+ const blip = firstChild(element, "a:buBlip");
1193
+ const font = firstChild(element, "a:buFont");
1194
+ const szPct = firstChild(element, "a:buSzPct");
1195
+ const szPts = firstChild(element, "a:buSzPts");
1196
+ const clr = firstChild(element, "a:buClr");
1197
+ let buAutoNum;
1198
+ if (autoNum !== void 0) {
1199
+ const type = attributeValue(autoNum, "type");
1200
+ if (type === void 0) throw new ModelError("MODEL_TEXT_BULLET", "a:buAutoNum has no @type, which the schema requires", partName, "a:buAutoNum");
1201
+ buAutoNum = {
1202
+ type,
1203
+ startAt: intOf$1(autoNum, "startAt", partName)
1204
+ };
1205
+ }
1206
+ let buBlip;
1207
+ if (blip !== void 0) {
1208
+ const embed = firstChild(blip, "a:blip");
1209
+ buBlip = embed === void 0 ? void 0 : attributeValue(embed, "r:embed");
1210
+ if (buBlip === void 0) throw new ModelError("MODEL_TEXT_BULLET", "a:buBlip has no a:blip/@r:embed to resolve", partName, "a:buBlip");
1211
+ }
1212
+ let buFont;
1213
+ if (firstChild(element, "a:buFontTx") !== void 0) buFont = { kind: "text" };
1214
+ else if (font !== void 0) buFont = {
1215
+ kind: "typeface",
1216
+ value: typefaceOf(font, partName)
1217
+ };
1218
+ let buSize;
1219
+ if (firstChild(element, "a:buSzTx") !== void 0) buSize = { kind: "text" };
1220
+ else if (szPct !== void 0) buSize = {
1221
+ kind: "percent",
1222
+ value: requiredPercentage(szPct, partName, "a:buSzPct")
1223
+ };
1224
+ else if (szPts !== void 0) buSize = {
1225
+ kind: "points",
1226
+ value: requiredInt(szPts, partName, "a:buSzPts")
1227
+ };
1228
+ let buColor;
1229
+ if (firstChild(element, "a:buClrTx") !== void 0) buColor = { kind: "text" };
1230
+ else if (clr !== void 0) {
1231
+ const value = parseColorChild(clr);
1232
+ if (value === void 0 || value === null) throw new ModelError("MODEL_TEXT_BULLET", "a:buClr holds no colour", partName, "a:buClr");
1233
+ buColor = {
1234
+ kind: "color",
1235
+ value
1236
+ };
1237
+ }
1238
+ return {
1239
+ buKind,
1240
+ buChar: charElement === void 0 ? void 0 : attributeValue(charElement, "char") ?? "",
1241
+ buAutoNum,
1242
+ buBlip,
1243
+ buFont,
1244
+ buSize,
1245
+ buColor
1246
+ };
1247
+ }
1248
+ /** `@val` on a `a:buSzPct`, which the schema makes required. */
1249
+ function requiredPercentage(element, partName, what) {
1250
+ const raw = attributeValue(element, "val");
1251
+ if (raw === void 0) throw new ModelError("MODEL_TEXT_BULLET", `${what} has no @val`, partName, what);
1252
+ return parsePercentage(raw);
1253
+ }
1254
+ /** `@val` on a `a:buSzPts`, in hundredths of a point. */
1255
+ function requiredInt(element, partName, what) {
1256
+ const value = intOf$1(element, "val", partName);
1257
+ if (value === void 0) throw new ModelError("MODEL_TEXT_BULLET", `${what} has no @val`, partName, what);
1258
+ return value;
1259
+ }
1260
+ const LEVEL_NAMES = Array.from({ length: 9 }, (_, i) => `a:lvl${String(i + 1)}pPr`);
1261
+ /**
1262
+ * `a:lstStyle`, or a `p:txStyles` bucket, which is the same type unwrapped.
1263
+ *
1264
+ * The nine levels are independent: measured in 3.1, a layout placeholder
1265
+ * declaring only `a:lvl2pPr` changes the second level and leaves the first and
1266
+ * third to the master. So a level nobody declared stays `undefined` rather than
1267
+ * falling back to level one, and the resolver asks per level.
1268
+ */
1269
+ function parseListStyle(element, partName) {
1270
+ const levels = [];
1271
+ for (const name of LEVEL_NAMES) {
1272
+ const level = firstChild(element, name);
1273
+ levels.push(level === void 0 ? void 0 : parseParaProps(level, partName));
1274
+ }
1275
+ const defPPr = firstChild(element, "a:defPPr");
1276
+ return {
1277
+ levels,
1278
+ defPPr: defPPr === void 0 ? void 0 : parseParaProps(defPPr, partName),
1279
+ node: element
1280
+ };
1281
+ }
1282
+ function parseListStyleChild(parent, qname, partName) {
1283
+ const element = firstChild(parent, qname);
1284
+ return element === void 0 ? void 0 : parseListStyle(element, partName);
1285
+ }
1286
+ /** `p:txStyles` on a master. */
1287
+ function parseTextStyles(element, partName) {
1288
+ return {
1289
+ title: parseListStyleChild(element, "p:titleStyle", partName),
1290
+ body: parseListStyleChild(element, "p:bodyStyle", partName),
1291
+ other: parseListStyleChild(element, "p:otherStyle", partName),
1292
+ node: element
1293
+ };
1294
+ }
1295
+ /** `p:defaultTextStyle` on `ppt/presentation.xml`, which is a `CT_TextListStyle`. */
1296
+ function parseDefaultTextStyle(presentation, partName) {
1297
+ return parseListStyleChild(presentation, "p:defaultTextStyle", partName);
1298
+ }
1299
+ /**
1300
+ * The paragraph's level, from `a:pPr/@lvl`.
1301
+ *
1302
+ * `ST_TextIndentLevelType` bounds it at 0..8 and PowerPoint still opens a file
1303
+ * outside the bound, so this clamps rather than throws - refusing to render a
1304
+ * deck PowerPoint renders is a worse answer than rendering it the way
1305
+ * PowerPoint does. A non-integer is a different matter and does throw: that is
1306
+ * a file saying something nobody can act on.
1307
+ */
1308
+ function levelOf(pPr, partName) {
1309
+ if (pPr === void 0) return 0;
1310
+ const lvl = intOf$1(pPr, "lvl", partName);
1311
+ if (lvl === void 0) return 0;
1312
+ return Math.max(0, Math.min(8, lvl));
1313
+ }
1314
+ function parseContent(element, partName) {
1315
+ if (element.qname === "a:r") {
1316
+ const t = firstChild(element, "a:t");
1317
+ return {
1318
+ kind: "run",
1319
+ props: parseRunPropsChild(element, "a:rPr", partName),
1320
+ text: t === void 0 ? "" : textContent(t),
1321
+ node: element
1322
+ };
1323
+ }
1324
+ if (element.qname === "a:br") return {
1325
+ kind: "br",
1326
+ props: parseRunPropsChild(element, "a:rPr", partName),
1327
+ node: element
1328
+ };
1329
+ if (element.qname === "a:fld") {
1330
+ const id = attributeValue(element, "id");
1331
+ if (id === void 0) throw new ModelError("MODEL_TEXT_FIELD", "a:fld has no @id", partName, "a:fld");
1332
+ const t = firstChild(element, "a:t");
1333
+ return {
1334
+ kind: "field",
1335
+ id,
1336
+ fieldType: attributeValue(element, "type"),
1337
+ props: parseRunPropsChild(element, "a:rPr", partName),
1338
+ text: t === void 0 ? "" : textContent(t),
1339
+ node: element
1340
+ };
1341
+ }
1342
+ return null;
1343
+ }
1344
+ function parseParagraph(element, partName) {
1345
+ const pPr = firstChild(element, "a:pPr");
1346
+ const content = [];
1347
+ for (const child of childElements(element)) {
1348
+ const parsed = parseContent(child, partName);
1349
+ if (parsed !== null) content.push(parsed);
1350
+ }
1351
+ return {
1352
+ props: pPr === void 0 ? void 0 : parseParaProps(pPr, partName),
1353
+ level: levelOf(pPr, partName),
1354
+ content,
1355
+ endParaRPr: parseRunPropsChild(element, "a:endParaRPr", partName),
1356
+ node: element
1357
+ };
1358
+ }
1359
+ /** A `p:txBody` or an `a:txBody`. */
1360
+ function parseTextBody(element, partName) {
1361
+ return {
1362
+ bodyPr: parseBodyPropsChild(element, partName),
1363
+ lstStyle: parseListStyleChild(element, "a:lstStyle", partName),
1364
+ paragraphs: childElements(element).filter((child) => child.qname === "a:p").map((child) => parseParagraph(child, partName)),
1365
+ node: element
1366
+ };
1367
+ }
1368
+ /** The `p:txBody` of a shape, when it has one. */
1369
+ function parseTextBodyChild(parent, partName) {
1370
+ const element = firstChild(parent, "p:txBody") ?? firstChild(parent, "a:txBody");
1371
+ return element === void 0 ? void 0 : parseTextBody(element, partName);
1372
+ }
1373
+ //#endregion
1374
+ //#region src/parse/sheet.ts
1375
+ /**
1376
+ * A slide, layout or master part into a `Sheet`; a theme part into a `Theme`.
1377
+ *
1378
+ * Nothing here resolves anything and nothing here defaults anything that the
1379
+ * file could have said. The one exception is `showMasterShapes`, whose default
1380
+ * is `true` and which has no third state to preserve.
1381
+ */
1382
+ const SLOT_SET = new Set(SCHEME_SLOTS);
1383
+ /**
1384
+ * `p:clrMap` or `a:overrideClrMapping`: all twelve attributes, every time.
1385
+ *
1386
+ * Eleven is a repair prompt, so a map that is missing one is a broken file
1387
+ * rather than a partial opinion, and there is nothing sensible to default the
1388
+ * missing one to.
1389
+ */
1390
+ function parseClrMap(element, partName) {
1391
+ const out = {};
1392
+ for (const name of MAPPED_COLOR_NAMES) {
1393
+ const raw = attributeValue(element, name);
1394
+ if (raw === void 0) throw new ModelError("MODEL_CLRMAP_INCOMPLETE", `${element.qname} has no @${name}`, partName, name);
1395
+ if (!SLOT_SET.has(raw)) throw new ModelError("MODEL_CLRMAP_SLOT", `${element.qname}/@${name} is "${raw}", which is not a clrScheme slot`, partName, raw);
1396
+ out[name] = raw;
1397
+ }
1398
+ return out;
1399
+ }
1400
+ function parseClrMapOvr(parent, partName) {
1401
+ const element = firstChild(parent, "p:clrMapOvr");
1402
+ if (element === void 0) return void 0;
1403
+ const override = firstChild(element, "a:overrideClrMapping");
1404
+ if (override !== void 0) return {
1405
+ kind: "override",
1406
+ map: parseClrMap(override, partName)
1407
+ };
1408
+ return { kind: "inherit" };
1409
+ }
1410
+ function intOf(element, name, partName) {
1411
+ const raw = attributeValue(element, name);
1412
+ if (raw === void 0) return 0;
1413
+ const value = Number(raw);
1414
+ if (!Number.isInteger(value)) throw new ModelError("MODEL_XFRM_NUMBER", `${element.qname}/@${name} is "${raw}"`, partName, raw);
1415
+ return value;
1416
+ }
1417
+ /**
1418
+ * The transform, from whichever element holds it.
1419
+ *
1420
+ * A `p:graphicFrame` keeps its geometry in a **PresentationML** `p:xfrm` and
1421
+ * has no `p:spPr` at all - it is a different element in a different namespace
1422
+ * from the `a:xfrm` every other shape uses, and looking for the wrong one gives
1423
+ * every chart, table, diagram and OLE object a size of zero.
1424
+ */
1425
+ function parseXfrm(parent, partName, qname = "a:xfrm") {
1426
+ const element = firstChild(parent, qname);
1427
+ if (element === void 0) return void 0;
1428
+ const off = firstChild(element, "a:off");
1429
+ const ext = firstChild(element, "a:ext");
1430
+ const chOff = firstChild(element, "a:chOff");
1431
+ const chExt = firstChild(element, "a:chExt");
1432
+ return {
1433
+ x: off === void 0 ? 0 : intOf(off, "x", partName),
1434
+ y: off === void 0 ? 0 : intOf(off, "y", partName),
1435
+ cx: ext === void 0 ? 0 : intOf(ext, "cx", partName),
1436
+ cy: ext === void 0 ? 0 : intOf(ext, "cy", partName),
1437
+ rot: intOf(element, "rot", partName),
1438
+ flipH: attributeValue(element, "flipH") === "1",
1439
+ flipV: attributeValue(element, "flipV") === "1",
1440
+ child: chOff === void 0 && chExt === void 0 ? null : {
1441
+ x: chOff === void 0 ? 0 : intOf(chOff, "x", partName),
1442
+ y: chOff === void 0 ? 0 : intOf(chOff, "y", partName),
1443
+ cx: chExt === void 0 ? 0 : intOf(chExt, "cx", partName),
1444
+ cy: chExt === void 0 ? 0 : intOf(chExt, "cy", partName)
1445
+ }
1446
+ };
1447
+ }
1448
+ const TYPE_SET = new Set(PLACEHOLDER_TYPES);
1449
+ const SIZE_SET = /* @__PURE__ */ new Set([
1450
+ "full",
1451
+ "half",
1452
+ "quarter"
1453
+ ]);
1454
+ /** `2^32 - 1`, the top of `xsd:unsignedInt`, which is what `@idx` is. */
1455
+ const MAX_IDX = 4294967295;
1456
+ function parsePlaceholder(nvPr, partName) {
1457
+ if (nvPr === void 0) return null;
1458
+ const ph = firstChild(nvPr, "p:ph");
1459
+ if (ph === void 0) return null;
1460
+ const rawType = attributeValue(ph, "type");
1461
+ const rawIdx = attributeValue(ph, "idx");
1462
+ let idx = null;
1463
+ if (rawIdx !== void 0) {
1464
+ const value = Number(rawIdx);
1465
+ if (!Number.isInteger(value) || value < 0 || value > MAX_IDX) throw new ModelError("MODEL_PLACEHOLDER_IDX", `p:ph/@idx is "${rawIdx}", which is not an unsignedInt`, partName, rawIdx);
1466
+ idx = value;
1467
+ }
1468
+ const rawSize = attributeValue(ph, "sz");
1469
+ const orient = attributeValue(ph, "orient");
1470
+ return {
1471
+ type: rawType !== void 0 && TYPE_SET.has(rawType) ? rawType : null,
1472
+ idx,
1473
+ size: rawSize !== void 0 && SIZE_SET.has(rawSize) ? rawSize : null,
1474
+ orient: orient === "vert" ? "vert" : orient === "horz" ? "horz" : null,
1475
+ hasCustomPrompt: attributeValue(ph, "hasCustomPrompt") === "1"
1476
+ };
1477
+ }
1478
+ function parseStyleRef(element, partName) {
1479
+ if (element === void 0) return {
1480
+ idx: 0,
1481
+ color: null
1482
+ };
1483
+ const raw = attributeValue(element, "idx");
1484
+ const idx = raw === void 0 ? 0 : Number(raw);
1485
+ if (!Number.isInteger(idx) || idx < 0) throw new ModelError("MODEL_STYLE_IDX", `${element.qname}/@idx is "${String(raw)}"`, partName, raw ?? "");
1486
+ let color = null;
1487
+ for (const child of childElements(element)) if (child.qname.startsWith("a:") && child.qname.endsWith("Clr")) {
1488
+ color = parseColorElement(child);
1489
+ break;
1490
+ }
1491
+ return {
1492
+ idx,
1493
+ color
1494
+ };
1495
+ }
1496
+ function parseStyle(parent, partName) {
1497
+ const element = firstChild(parent, "p:style");
1498
+ if (element === void 0) return void 0;
1499
+ const fontRefElement = firstChild(element, "a:fontRef");
1500
+ const rawFont = fontRefElement === void 0 ? void 0 : attributeValue(fontRefElement, "idx");
1501
+ if (rawFont !== void 0 && rawFont !== "major" && rawFont !== "minor" && rawFont !== "none") throw new ModelError("MODEL_FONT_COLLECTION", `a:fontRef/@idx is "${rawFont}", not major, minor or none`, partName, rawFont);
1502
+ let fontColor = null;
1503
+ if (fontRefElement !== void 0) {
1504
+ for (const child of childElements(fontRefElement)) if (child.qname.startsWith("a:") && child.qname.endsWith("Clr")) {
1505
+ fontColor = parseColorElement(child);
1506
+ break;
1507
+ }
1508
+ }
1509
+ return {
1510
+ lnRef: parseStyleRef(firstChild(element, "a:lnRef"), partName),
1511
+ fillRef: parseStyleRef(firstChild(element, "a:fillRef"), partName),
1512
+ effectRef: parseStyleRef(firstChild(element, "a:effectRef"), partName),
1513
+ fontRef: {
1514
+ idx: rawFont ?? "minor",
1515
+ color: fontColor
1516
+ }
1517
+ };
1518
+ }
1519
+ const SHAPE_KINDS = {
1520
+ "p:sp": "sp",
1521
+ "p:pic": "pic",
1522
+ "p:grpSp": "grpSp",
1523
+ "p:graphicFrame": "graphicFrame",
1524
+ "p:cxnSp": "cxnSp",
1525
+ "p:contentPart": "contentPart"
1526
+ };
1527
+ /** Where each shape kind keeps its non-visual properties. */
1528
+ const NV_CONTAINERS = {
1529
+ sp: "p:nvSpPr",
1530
+ pic: "p:nvPicPr",
1531
+ grpSp: "p:nvGrpSpPr",
1532
+ graphicFrame: "p:nvGraphicFramePr",
1533
+ cxnSp: "p:nvCxnSpPr",
1534
+ contentPart: "p:nvContentPartPr"
1535
+ };
1536
+ function parseShape(element, kind, partName) {
1537
+ const nv = firstChild(element, NV_CONTAINERS[kind]);
1538
+ const cNvPr = nv === void 0 ? void 0 : firstChild(nv, "p:cNvPr");
1539
+ const spPr = firstChild(element, kind === "grpSp" ? "p:grpSpPr" : "p:spPr");
1540
+ const geometryHost = kind === "graphicFrame" ? element : spPr;
1541
+ const prstGeom = spPr === void 0 ? void 0 : firstChild(spPr, "a:prstGeom");
1542
+ const children = [];
1543
+ if (kind === "grpSp") for (const child of childElements(element)) {
1544
+ const childKind = SHAPE_KINDS[child.qname];
1545
+ if (childKind !== void 0) children.push(parseShape(child, childKind, partName));
1546
+ }
1547
+ const picture = kind === "pic" ? parsePictureFill(element, partName) : void 0;
1548
+ const rawId = cNvPr === void 0 ? void 0 : attributeValue(cNvPr, "id");
1549
+ return {
1550
+ kind,
1551
+ cNvPrId: rawId === void 0 ? 0 : Number(rawId),
1552
+ name: (cNvPr === void 0 ? void 0 : attributeValue(cNvPr, "name")) ?? "",
1553
+ descr: (cNvPr === void 0 ? void 0 : attributeValue(cNvPr, "descr")) ?? null,
1554
+ hidden: cNvPr !== void 0 && attributeValue(cNvPr, "hidden") === "1",
1555
+ placeholder: parsePlaceholder(nv === void 0 ? void 0 : firstChild(nv, "p:nvPr"), partName),
1556
+ xfrm: geometryHost === void 0 ? void 0 : parseXfrm(geometryHost, partName, kind === "graphicFrame" ? "p:xfrm" : "a:xfrm"),
1557
+ fill: picture ?? (spPr === void 0 ? void 0 : parseFill(spPr, partName)),
1558
+ line: spPr === void 0 ? void 0 : parseLine(spPr, partName),
1559
+ effects: spPr === void 0 ? void 0 : parseEffects(spPr),
1560
+ style: parseStyle(element, partName),
1561
+ prstGeom: prstGeom === void 0 ? void 0 : attributeValue(prstGeom, "prst") ?? "",
1562
+ geometry: parseGeometry(spPr, partName),
1563
+ text: parseTextBodyChild(element, partName),
1564
+ children,
1565
+ node: element
1566
+ };
1567
+ }
1568
+ function parseShapeTree(cSld, partName) {
1569
+ const tree = firstChild(cSld, "p:spTree");
1570
+ if (tree === void 0) return [];
1571
+ const out = [];
1572
+ for (const child of childElements(tree)) {
1573
+ const kind = SHAPE_KINDS[child.qname];
1574
+ if (kind !== void 0) out.push(parseShape(child, kind, partName));
1575
+ }
1576
+ return out;
1577
+ }
1578
+ function parseBackground(cSld, partName) {
1579
+ const bg = firstChild(cSld, "p:bg");
1580
+ if (bg === void 0) return void 0;
1581
+ const ref = firstChild(bg, "p:bgRef");
1582
+ if (ref !== void 0) return {
1583
+ kind: "ref",
1584
+ ref: parseStyleRef(ref, partName)
1585
+ };
1586
+ const pr = firstChild(bg, "p:bgPr");
1587
+ if (pr === void 0) return void 0;
1588
+ return {
1589
+ kind: "fill",
1590
+ fill: parseFill(pr, partName) ?? { type: "none" },
1591
+ effects: parseEffects(pr)
1592
+ };
1593
+ }
1594
+ function parseScheme(element, partName) {
1595
+ const out = {};
1596
+ for (const slot of SCHEME_SLOTS) {
1597
+ const child = firstChild(element, `a:${slot}`);
1598
+ if (child === void 0) continue;
1599
+ for (const grandchild of childElements(child)) if (grandchild.qname.startsWith("a:") && grandchild.qname.endsWith("Clr")) {
1600
+ out[slot] = parseColorElement(grandchild);
1601
+ break;
1602
+ }
1603
+ }
1604
+ const missing = SCHEME_SLOTS.filter((slot) => out[slot] === void 0);
1605
+ if (missing.length > 0) throw new ModelError("MODEL_CLRMAP_INCOMPLETE", `a:clrScheme is missing ${missing.join(", ")}`, partName, missing.join(","));
1606
+ return out;
1607
+ }
1608
+ function parseFontScheme(element) {
1609
+ const collection = (which) => {
1610
+ const font = element === void 0 ? void 0 : firstChild(element, which);
1611
+ const face = (script) => {
1612
+ if (font === void 0) return null;
1613
+ const child = firstChild(font, script);
1614
+ return child === void 0 ? null : attributeValue(child, "typeface") ?? null;
1615
+ };
1616
+ return {
1617
+ latin: face("a:latin"),
1618
+ ea: face("a:ea"),
1619
+ cs: face("a:cs")
1620
+ };
1621
+ };
1622
+ return {
1623
+ name: element === void 0 ? null : attributeValue(element, "name") ?? null,
1624
+ major: collection("a:majorFont"),
1625
+ minor: collection("a:minorFont")
1626
+ };
1627
+ }
1628
+ function parseFormatScheme(element, partName) {
1629
+ if (element === void 0) return null;
1630
+ const fills = (name) => {
1631
+ const list = firstChild(element, name);
1632
+ if (list === void 0) return [];
1633
+ return childElements(list).map((child) => parseFillElement(child, partName));
1634
+ };
1635
+ const lineList = firstChild(element, "a:lnStyleLst");
1636
+ const lines = lineList === void 0 ? [] : childElements(lineList).filter((child) => child.qname === "a:ln").map((child) => parseLineElement(child, partName));
1637
+ const effectList = firstChild(element, "a:effectStyleLst");
1638
+ const effects = effectList === void 0 ? [] : childElements(effectList).filter((child) => child.qname === "a:effectStyle").map((child) => parseEffects(child) ?? []);
1639
+ return {
1640
+ name: attributeValue(element, "name") ?? null,
1641
+ fillStyles: fills("a:fillStyleLst"),
1642
+ lineStyles: lines,
1643
+ effectStyles: effects,
1644
+ bgFillStyles: fills("a:bgFillStyleLst")
1645
+ };
1646
+ }
1647
+ function parseTheme(root, partName) {
1648
+ const elements = firstChild(root, "a:themeElements");
1649
+ const scheme = elements === void 0 ? void 0 : firstChild(elements, "a:clrScheme");
1650
+ if (elements === void 0 || scheme === void 0) throw new ModelError("MODEL_PART_KIND", "a theme with no a:clrScheme", partName);
1651
+ return {
1652
+ partName,
1653
+ name: attributeValue(root, "name") ?? null,
1654
+ scheme: parseScheme(scheme, partName),
1655
+ fonts: parseFontScheme(firstChild(elements, "a:fontScheme")),
1656
+ format: parseFormatScheme(firstChild(elements, "a:fmtScheme"), partName),
1657
+ node: root
1658
+ };
1659
+ }
1660
+ const ROOTS = {
1661
+ "p:sld": "slide",
1662
+ "p:sldLayout": "layout",
1663
+ "p:sldMaster": "master"
1664
+ };
1665
+ /**
1666
+ * One part into one sheet. The parent and the theme are filled in by
1667
+ * `document.ts`, which is the only thing that can follow a relationship.
1668
+ */
1669
+ function parseSheet(root, partName) {
1670
+ const kind = ROOTS[root.qname];
1671
+ if (kind === void 0) throw new ModelError("MODEL_PART_KIND", `${root.qname} is not a sheet root`, partName);
1672
+ const cSld = firstChild(root, "p:cSld");
1673
+ if (cSld === void 0) throw new ModelError("MODEL_PART_KIND", `${root.qname} has no p:cSld`, partName);
1674
+ const clrMapElement = kind === "master" ? firstChild(root, "p:clrMap") : void 0;
1675
+ const txStylesElement = kind === "master" ? firstChild(root, "p:txStyles") : void 0;
1676
+ return {
1677
+ kind,
1678
+ partName,
1679
+ name: attributeValue(cSld, "name") ?? null,
1680
+ layoutType: kind === "layout" ? attributeValue(root, "type") ?? null : null,
1681
+ matchingName: kind === "layout" ? attributeValue(root, "matchingName") ?? null : null,
1682
+ shapes: parseShapeTree(cSld, partName),
1683
+ background: parseBackground(cSld, partName),
1684
+ clrMap: clrMapElement === void 0 ? void 0 : parseClrMap(clrMapElement, partName),
1685
+ txStyles: txStylesElement === void 0 ? void 0 : parseTextStyles(txStylesElement, partName),
1686
+ clrMapOvr: kind === "master" ? void 0 : parseClrMapOvr(root, partName),
1687
+ showMasterShapes: attributeValue(root, "showMasterSp") !== "0",
1688
+ node: root
1689
+ };
1690
+ }
1691
+ //#endregion
1692
+ //#region src/resolve/placeholder.ts
1693
+ /**
1694
+ * The type of a `p:ph` that has none.
1695
+ *
1696
+ * `obj`, measured: a bare `<p:ph/>` on a slide reads back through PowerPoint's
1697
+ * object model as `ppPlaceholderObject`. ECMA's own default for the attribute
1698
+ * is `body`, and the two behave identically at the first hop because that hop
1699
+ * ignores the type - but they differ at the second, where `obj` folds to `body`
1700
+ * and `body` is already there. They agree, and it is worth knowing why rather
1701
+ * than by luck.
1702
+ */
1703
+ const DEFAULT_PLACEHOLDER_TYPE = "obj";
1704
+ /** The `@idx` of a `p:ph` that has none. */
1705
+ const DEFAULT_PLACEHOLDER_IDX = 0;
1706
+ /** Supply the two defaults, so a comparison has something to compare. */
1707
+ function normalizePlaceholder(ph) {
1708
+ return {
1709
+ type: ph.type ?? "obj",
1710
+ idx: ph.idx ?? 0
1711
+ };
1712
+ }
1713
+ /** The four types that fold to `title` or stay as themselves on a master. */
1714
+ const MASTER_TITLE = /* @__PURE__ */ new Set(["title", "ctrTitle"]);
1715
+ /** The types a master may carry, which are the only targets of the second hop. */
1716
+ const MASTER_OWN = /* @__PURE__ */ new Set([
1717
+ "body",
1718
+ "dt",
1719
+ "ftr",
1720
+ "sldNum",
1721
+ "hdr"
1722
+ ]);
1723
+ /**
1724
+ * The type a placeholder presents when it asks a master for its parent.
1725
+ *
1726
+ * `ctrTitle` becomes `title`; everything a master may not carry - `subTitle`,
1727
+ * `obj`, `pic`, `chart`, `tbl`, `clipArt`, `dgm`, `media`, `sldImg` - becomes
1728
+ * `body`. Measured on `pic`, `obj`, `subTitle` and `ctrTitle`; the rest follow
1729
+ * because a master cannot hold them either and `body` is the only remaining
1730
+ * content type.
1731
+ */
1732
+ function masterPlaceholderType(type) {
1733
+ if (MASTER_TITLE.has(type)) return "title";
1734
+ if (MASTER_OWN.has(type)) return type;
1735
+ return "body";
1736
+ }
1737
+ /** Every placeholder in a sheet's top-level shape tree, in document order. */
1738
+ function placeholders(sheet) {
1739
+ return sheet.shapes.filter((shape) => shape.placeholder !== null);
1740
+ }
1741
+ /**
1742
+ * Slide to layout: the first placeholder whose `@idx` agrees.
1743
+ *
1744
+ * "First" matters only for a layout that has two placeholders at one index,
1745
+ * which PowerPoint accepts without repairing; the first in the shape tree is
1746
+ * the one that wins.
1747
+ */
1748
+ function matchInLayout(ph, layout) {
1749
+ const want = normalizePlaceholder(ph);
1750
+ for (const shape of layout.shapes) {
1751
+ if (shape.placeholder === null) continue;
1752
+ if (normalizePlaceholder(shape.placeholder).idx === want.idx) return shape;
1753
+ }
1754
+ return null;
1755
+ }
1756
+ /** Layout to master: the first placeholder of the folded type. */
1757
+ function matchInMaster(ph, master) {
1758
+ const want = masterPlaceholderType(normalizePlaceholder(ph).type);
1759
+ for (const shape of master.shapes) {
1760
+ if (shape.placeholder === null) continue;
1761
+ if (masterPlaceholderType(normalizePlaceholder(shape.placeholder).type) === want) return shape;
1762
+ }
1763
+ return null;
1764
+ }
1765
+ /**
1766
+ * The chain a shape inherits along, nearest first.
1767
+ *
1768
+ * One entry for a non-placeholder, and for a placeholder that matched nothing.
1769
+ * Up to three for a slide placeholder that reached the master. Each hop asks
1770
+ * with the placeholder of the sheet it is leaving, which is why the layout's
1771
+ * `@type` and not the slide's decides the second hop.
1772
+ */
1773
+ function inheritanceChain(shape, sheet) {
1774
+ const chain = [{
1775
+ shape,
1776
+ sheet,
1777
+ origin: "shape"
1778
+ }];
1779
+ if (shape.placeholder === null) return chain;
1780
+ let current = shape;
1781
+ let currentSheet = sheet;
1782
+ const seen = /* @__PURE__ */ new Set([sheet.partName]);
1783
+ while (currentSheet.parent !== null && current.placeholder !== null) {
1784
+ const parentSheet = currentSheet.parent;
1785
+ if (seen.has(parentSheet.partName)) break;
1786
+ seen.add(parentSheet.partName);
1787
+ const next = parentSheet.kind === "master" ? matchInMaster(current.placeholder, parentSheet) : matchInLayout(current.placeholder, parentSheet);
1788
+ if (next === null) break;
1789
+ chain.push({
1790
+ shape: next,
1791
+ sheet: parentSheet,
1792
+ origin: parentSheet.kind === "master" ? "masterPh" : "layoutPh"
1793
+ });
1794
+ current = next;
1795
+ currentSheet = parentSheet;
1796
+ }
1797
+ return chain;
1798
+ }
1799
+ /**
1800
+ * A placeholder that reached no parent and states no geometry of its own.
1801
+ *
1802
+ * PowerPoint renders one at the origin with zero width and height - it does not
1803
+ * refuse the file, does not drop the shape, and does not invent a rectangle.
1804
+ * Measured on four probes: a title where the layout has none, a body at an index
1805
+ * nobody has, and both against a layout with no placeholders at all.
1806
+ */
1807
+ const ORPHAN_RECT = {
1808
+ x: 0,
1809
+ y: 0,
1810
+ cx: 0,
1811
+ cy: 0
1812
+ };
1813
+ //#endregion
1814
+ //#region src/resolve/resolve.ts
1815
+ /**
1816
+ * The first level of the chain that declared something.
1817
+ *
1818
+ * `pick` must return `undefined` for "this level did not say" and anything else
1819
+ * for a value, including `null` - a shape may legitimately declare a `null`, and
1820
+ * conflating that with silence is how an explicit `a:noFill` gets overwritten by
1821
+ * an inherited colour.
1822
+ */
1823
+ function resolve(shape, sheet, pick) {
1824
+ const chain = inheritanceChain(shape, sheet);
1825
+ for (const [index, link] of chain.entries()) {
1826
+ const value = pick(link.shape);
1827
+ if (value === void 0) continue;
1828
+ return {
1829
+ value,
1830
+ origin: link.origin,
1831
+ explicit: index === 0,
1832
+ sheet: link.sheet,
1833
+ shape: link.shape
1834
+ };
1835
+ }
1836
+ }
1837
+ /**
1838
+ * The rectangle a shape occupies, and which sheet stated it.
1839
+ *
1840
+ * The single most consequential call in the package. A slide placeholder that
1841
+ * PowerPoint wrote has **no** `a:xfrm` - not when it was created, not when text
1842
+ * was typed into it, only when the user moved or resized it, at which point the
1843
+ * whole resolved rectangle is baked in at once. So an absent `xfrm` is the
1844
+ * statement "still bound to my layout", and it is what Change Layout reads.
1845
+ */
1846
+ function resolveXfrm(shape, sheet) {
1847
+ return resolve(shape, sheet, (s) => s.xfrm);
1848
+ }
1849
+ /** Every sheet from this one up to its master, nearest first. */
1850
+ function sheetChain(sheet) {
1851
+ const chain = [];
1852
+ const seen = /* @__PURE__ */ new Set();
1853
+ let current = sheet;
1854
+ while (current !== null) {
1855
+ if (seen.has(current.partName)) throw new ModelError("MODEL_SHEET_CYCLE", `the sheet chain returns to ${current.partName}`, current.partName);
1856
+ seen.add(current.partName);
1857
+ chain.push(current);
1858
+ current = current.parent;
1859
+ }
1860
+ return chain;
1861
+ }
1862
+ /** The master at the top of a sheet's chain, or `null` if the chain is broken. */
1863
+ function masterOf(sheet) {
1864
+ for (const link of sheetChain(sheet)) if (link.kind === "master") return link;
1865
+ return null;
1866
+ }
1867
+ /**
1868
+ * The theme a sheet resolves against.
1869
+ *
1870
+ * The master's, reached through this sheet's own chain. **Not** the one
1871
+ * `ppt/presentation.xml.rels` names: PowerPoint writes a `theme` relationship
1872
+ * there too, it always points at `theme1.xml`, and on a two-master deck every
1873
+ * slide on the second master would take the first master's palette. Measured -
1874
+ * the two masters resolved `accent1` to two different colours, and each
1875
+ * master's background used its own theme's `bgFillStyleLst`.
1876
+ *
1877
+ * A master must own its theme part outright: two masters pointing at the same
1878
+ * one is repaired on open, so a shared theme is a broken file rather than an
1879
+ * economy.
1880
+ */
1881
+ function themeOf(sheet) {
1882
+ return masterOf(sheet)?.theme ?? null;
1883
+ }
1884
+ function schemeOf(sheet) {
1885
+ return themeOf(sheet)?.scheme;
1886
+ }
1887
+ /**
1888
+ * The colour map in force on a sheet.
1889
+ *
1890
+ * `a:masterClrMapping` says "the map already in force", and the surprise is what
1891
+ * that means: for a slide it is the **layout's** map, not the master's. Measured
1892
+ * - a layout carrying an `a:overrideClrMapping` changed `accent1` on a slide
1893
+ * that declared `masterClrMapping`, and a slide with an override of its own beat
1894
+ * its layout's. The element is named for the sheet it usually ends at rather
1895
+ * than for the one it asks.
1896
+ */
1897
+ function colorMapOf(sheet) {
1898
+ for (const link of sheetChain(sheet)) {
1899
+ if (link.kind === "master") return link.clrMap;
1900
+ if (link.clrMapOvr?.kind === "override") return link.clrMapOvr.map;
1901
+ }
1902
+ }
1903
+ /**
1904
+ * Everything `resolveColor` needs, for a colour written on this sheet.
1905
+ *
1906
+ * `phClr` is supplied by whatever style invocation the colour sits inside, and
1907
+ * there is nothing sensible to default it to: `style.ts` passes it in, and a
1908
+ * `schemeClr val="phClr"` reached any other way throws rather than painting
1909
+ * black.
1910
+ */
1911
+ function colorContextOf(sheet, phClr) {
1912
+ const scheme = schemeOf(sheet);
1913
+ const map = colorMapOf(sheet);
1914
+ return {
1915
+ ...scheme === void 0 ? {} : { scheme },
1916
+ ...map === void 0 ? {} : { map },
1917
+ ...phClr === void 0 ? {} : { phClr }
1918
+ };
1919
+ }
1920
+ //#endregion
1921
+ //#region src/style.ts
1922
+ /**
1923
+ * The style matrix, the 1000-offset, and `phClr`.
1924
+ *
1925
+ * A `p:style` holds four references into the theme. Each names an entry by
1926
+ * index and carries the colour that entry's `schemeClr val="phClr"` is to be
1927
+ * resolved with - so an entry such as
1928
+ * `<a:solidFill><a:schemeClr val="phClr"><a:lumMod val="60000"/></a:schemeClr></a:solidFill>`
1929
+ * is a *function* of a colour, and the reference is the call.
1930
+ *
1931
+ * Miss `phClr` and every themed shape renders black, which is the failure this
1932
+ * whole file exists to prevent.
1933
+ */
1934
+ /**
1935
+ * The offset that separates the two style lists.
1936
+ *
1937
+ * `0` and `1000` mean *nothing at all*; `1..999` index the main list; `1001` and
1938
+ * up index the background list. Measured on both `a:fillRef` and `p:bgRef`
1939
+ * against a theme whose six entries paint six distinguishable colours - all six
1940
+ * agreed between the two elements, and `idx="0"` and `idx="1000"` both painted
1941
+ * nothing.
1942
+ */
1943
+ const STYLE_MATRIX_OFFSET = 1e3;
1944
+ /**
1945
+ * Where an index points, before anything is fetched.
1946
+ *
1947
+ * Out of range is not an error. A `fillRef idx="4"` against a three-entry list
1948
+ * paints the third entry, and a `bgRef idx="9999"` paints the last background
1949
+ * entry: PowerPoint clamps, opens the file without repairing it, and a renderer
1950
+ * that throws here refuses a deck PowerPoint shows.
1951
+ */
1952
+ function styleMatrixTarget(idx) {
1953
+ if (idx === 0 || idx === 1e3) return { list: "none" };
1954
+ if (idx < 1e3) return {
1955
+ list: "main",
1956
+ at: idx - 1
1957
+ };
1958
+ return {
1959
+ list: "background",
1960
+ at: idx - STYLE_MATRIX_OFFSET - 1
1961
+ };
1962
+ }
1963
+ function clampInto(entries, at, what, theme) {
1964
+ if (entries.length === 0) throw new ModelError("MODEL_STYLE_LIST_EMPTY", `${what} has no entries`, theme.partName, what);
1965
+ return entries[Math.min(Math.max(at, 0), entries.length - 1)];
1966
+ }
1967
+ /** The fill a `fillRef`/`bgRef` index names, or `null` for none. */
1968
+ function styleMatrixFill(ref, theme) {
1969
+ const target = styleMatrixTarget(ref.idx);
1970
+ if (target.list === "none") return null;
1971
+ const format = theme.format;
1972
+ if (format === null) throw new ModelError("MODEL_NO_STYLE_MATRIX", "the theme has no a:fmtScheme", theme.partName);
1973
+ return target.list === "main" ? clampInto(format.fillStyles, target.at, "a:fillStyleLst", theme) : clampInto(format.bgFillStyles, target.at, "a:bgFillStyleLst", theme);
1974
+ }
1975
+ /**
1976
+ * The line an `lnRef` index names, or `null` for none.
1977
+ *
1978
+ * `a:lnStyleLst` has no background counterpart, so an index above 1000 clamps
1979
+ * into the same list rather than reaching a second one.
1980
+ */
1981
+ function styleMatrixLine(ref, theme) {
1982
+ const target = styleMatrixTarget(ref.idx);
1983
+ if (target.list === "none") return null;
1984
+ const format = theme.format;
1985
+ if (format === null) throw new ModelError("MODEL_NO_STYLE_MATRIX", "the theme has no a:fmtScheme", theme.partName);
1986
+ return clampInto(format.lineStyles, target.at, "a:lnStyleLst", theme);
1987
+ }
1988
+ /** The effects an `effectRef` index names. Empty for none. */
1989
+ function styleMatrixEffects(ref, theme) {
1990
+ const target = styleMatrixTarget(ref.idx);
1991
+ if (target.list === "none") return [];
1992
+ const format = theme.format;
1993
+ if (format === null) throw new ModelError("MODEL_NO_STYLE_MATRIX", "the theme has no a:fmtScheme", theme.partName);
1994
+ return clampInto(format.effectStyles, target.at, "a:effectStyleLst", theme);
1995
+ }
1996
+ /**
1997
+ * The colour a style reference invokes its entry with.
1998
+ *
1999
+ * The reference's own colour child, resolved against the sheet - and it may
2000
+ * carry transforms of its own. PowerPoint's shape-style gallery writes
2001
+ * `<a:schemeClr val="accent1"><a:shade val="15000"/></a:schemeClr>` on seven of
2002
+ * its forty-two entries, so `phClr` is a fully transformed colour and not a
2003
+ * theme slot name.
2004
+ */
2005
+ function phClrOf(ref, sheet) {
2006
+ if (ref.color === null) return null;
2007
+ return resolveColor(ref.color, colorContextOf(sheet));
2008
+ }
2009
+ function styleOf(shape, sheet) {
2010
+ return resolve(shape, sheet, (s) => s.style);
2011
+ }
2012
+ /**
2013
+ * What a shape paints, following both routes and in the right order.
2014
+ *
2015
+ * An explicit `spPr` fill beats the `fillRef`, and an explicit `a:noFill` beats
2016
+ * it too - both measured. A shape with neither paints nothing rather than black.
2017
+ *
2018
+ * The two routes are searched independently: the `spPr` fill is looked for down
2019
+ * the whole inheritance chain first, and only if no level declared one does the
2020
+ * `p:style` - itself inherited down the same chain - get consulted.
2021
+ */
2022
+ function resolveAppearance(shape, sheet) {
2023
+ const theme = themeOf(sheet);
2024
+ const style = styleOf(shape, sheet);
2025
+ const directFill = resolve(shape, sheet, (s) => s.fill);
2026
+ const directLine = resolve(shape, sheet, (s) => s.line);
2027
+ const directEffects = resolve(shape, sheet, (s) => s.effects);
2028
+ let fill = directFill?.value ?? null;
2029
+ let fillOrigin = directFill?.origin ?? null;
2030
+ let fillPhClr = null;
2031
+ if (directFill === void 0 && style !== void 0 && theme !== null) {
2032
+ const ref = style.value?.fillRef;
2033
+ if (ref !== void 0) {
2034
+ fill = styleMatrixFill(ref, theme);
2035
+ fillOrigin = fill === null ? null : "theme";
2036
+ fillPhClr = fill === null ? null : phClrOf(ref, sheet);
2037
+ }
2038
+ }
2039
+ let line = directLine?.value ?? null;
2040
+ let lineOrigin = directLine?.origin ?? null;
2041
+ let linePhClr = null;
2042
+ if (directLine === void 0 && style !== void 0 && theme !== null) {
2043
+ const ref = style.value?.lnRef;
2044
+ if (ref !== void 0) {
2045
+ line = styleMatrixLine(ref, theme);
2046
+ lineOrigin = line === null ? null : "theme";
2047
+ linePhClr = line === null ? null : phClrOf(ref, sheet);
2048
+ }
2049
+ }
2050
+ let effects = directEffects?.value ?? null;
2051
+ if (directEffects === void 0 && style !== void 0 && theme !== null) {
2052
+ const ref = style.value?.effectRef;
2053
+ if (ref !== void 0) effects = styleMatrixEffects(ref, theme);
2054
+ }
2055
+ return {
2056
+ fill,
2057
+ fillOrigin,
2058
+ fillPhClr,
2059
+ line,
2060
+ lineOrigin,
2061
+ linePhClr,
2062
+ effects
2063
+ };
2064
+ }
2065
+ /**
2066
+ * A shape's fill as a single colour, when it is one.
2067
+ *
2068
+ * A convenience over `resolveAppearance` for the common case, and the shape the
2069
+ * ground-truth fixture is in: PowerPoint's object model reports one RGB per
2070
+ * shape, so this is what a measurement can be compared against.
2071
+ */
2072
+ function resolveSolidFill(shape, sheet) {
2073
+ const appearance = resolveAppearance(shape, sheet);
2074
+ if (appearance.fill === null || appearance.fill.type !== "solid") return null;
2075
+ const phClr = appearance.fillPhClr;
2076
+ return resolveColor(appearance.fill.color, colorContextOf(sheet, phClr === null ? void 0 : phClr));
2077
+ }
2078
+ /** Resolve any colour written on a sheet, with an optional `phClr` in scope. */
2079
+ function resolveOnSheet(color, sheet, phClr) {
2080
+ return resolveColor(color, colorContextOf(sheet, phClr));
2081
+ }
2082
+ //#endregion
2083
+ //#region src/resolve/background.ts
2084
+ /**
2085
+ * What is behind everything on a slide.
2086
+ *
2087
+ * Two questions, and they are independent. *Which sheet's* background applies,
2088
+ * and *what* that background is.
2089
+ *
2090
+ * **Which sheet.** The nearest one that declares a `p:bg` at all: the slide,
2091
+ * else its layout, else the master. Measured on all three. An absent `p:bg` is
2092
+ * the only thing that inherits - a slide declaring `<p:bgPr><a:noFill/>` paints
2093
+ * nothing and does *not* fall through, which is the distinction between "I have
2094
+ * no opinion" and "my opinion is nothing", and the reason `Background` is a
2095
+ * value rather than a nullable fill.
2096
+ *
2097
+ * **What it is.** `p:bgPr` states a fill outright. `p:bgRef` names an entry of
2098
+ * the theme's style matrix and the colour to invoke its `phClr` with, using the
2099
+ * same 1000-offset as `a:fillRef` over the same two lists - measured on six
2100
+ * indices through both elements, agreeing on all six. PowerPoint's own master
2101
+ * writes `<p:bgRef idx="1001"><a:schemeClr val="bg1"/></p:bgRef>`, which is the
2102
+ * first entry of `a:bgFillStyleLst` invoked with whatever `bg1` maps to.
2103
+ *
2104
+ * And `bg1` there is resolved through the colour map in force **on the sheet
2105
+ * that owns the shape**, not on the sheet that owns the background: a slide
2106
+ * whose `clrMapOvr` remaps `bg1` gets a different background out of the same
2107
+ * master. Measured.
2108
+ */
2109
+ /** Which sheet's `p:bg` applies, without unpacking it. */
2110
+ function backgroundSheet(sheet) {
2111
+ for (const link of sheetChain(sheet)) if (link.background !== void 0) return {
2112
+ sheet: link,
2113
+ background: link.background
2114
+ };
2115
+ return null;
2116
+ }
2117
+ /**
2118
+ * The background of a slide, layout or master.
2119
+ *
2120
+ * `null` when no sheet in the chain declares one, which is a legal file: a deck
2121
+ * whose master has no `p:bg` paints nothing behind its slides.
2122
+ */
2123
+ function resolveBackground(sheet) {
2124
+ const found = backgroundSheet(sheet);
2125
+ if (found === null) return null;
2126
+ if (found.background.kind === "fill") return {
2127
+ fill: found.background.fill,
2128
+ sheet: found.sheet,
2129
+ explicit: found.sheet === sheet,
2130
+ form: "bgPr",
2131
+ phClr: null
2132
+ };
2133
+ const theme = themeOf(sheet);
2134
+ if (theme === null) return null;
2135
+ const fill = styleMatrixFill(found.background.ref, theme);
2136
+ if (fill === null) return {
2137
+ fill: { type: "none" },
2138
+ sheet: found.sheet,
2139
+ explicit: found.sheet === sheet,
2140
+ form: "bgRef",
2141
+ phClr: null
2142
+ };
2143
+ return {
2144
+ fill,
2145
+ sheet: found.sheet,
2146
+ explicit: found.sheet === sheet,
2147
+ form: "bgRef",
2148
+ phClr: phClrOf(found.background.ref, sheet)
2149
+ };
2150
+ }
2151
+ /** The background as one colour, when it is a solid. `null` otherwise. */
2152
+ function resolveBackgroundColor(sheet) {
2153
+ const background = resolveBackground(sheet);
2154
+ if (background === null || background.fill.type !== "solid") return null;
2155
+ return resolveColor(background.fill.color, colorContextOf(sheet, background.phClr === null ? void 0 : background.phClr));
2156
+ }
2157
+ //#endregion
2158
+ //#region src/document.ts
2159
+ /**
2160
+ * The package into sheets, and the sheets into a chain.
2161
+ *
2162
+ * ## Every binding comes from the part's own `.rels`, and from nothing else
2163
+ *
2164
+ * A slide part does not name its layout. A layout does not name its master. A
2165
+ * master does not name its theme. Each of those bindings lives in exactly one
2166
+ * place - `ppt/slides/_rels/slideN.xml.rels`, and its two counterparts - and
2167
+ * every other candidate is a trap:
2168
+ *
2169
+ * - **`p:sldLayoutIdLst` is not the binding.** It gives each layout an id and an
2170
+ * order for the UI. A two-master deck saved by PowerPoint puts all 22 layouts
2171
+ * in one flat `ppt/slideLayouts/` folder, numbered 1 to 22, and which master
2172
+ * owns which is recorded only in the two masters' relationship parts.
2173
+ * - **The part number is not the binding.** Same reason.
2174
+ * - **`ppt/presentation.xml.rels` has a `theme` relationship**, it always points
2175
+ * at `theme1.xml`, and reading it gives every slide the first master's palette.
2176
+ * On a one-master deck - which is every deck most people ever test with - that
2177
+ * is indistinguishable from correct.
2178
+ * - **The order of a `.rels` file is not `rId` order.** PowerPoint writes them
2179
+ * in whatever order it likes; `rId8` routinely comes first. Anything that
2180
+ * reads a relationship part positionally is wrong on the files Office writes.
2181
+ *
2182
+ * ## A broken binding is not an error
2183
+ *
2184
+ * A slide with no layout relationship, a layout with no master, a master with no
2185
+ * theme: PowerPoint repairs all three rather than refusing them, and after the
2186
+ * repair the binding is simply gone. So this loads them with `parent` or `theme`
2187
+ * as `null` and records the problem, rather than throwing. Refusing to *write*
2188
+ * one is `validate`'s job, and it is a different question.
2189
+ */
2190
+ const REL_SLIDE = REL_TYPE.slide;
2191
+ const REL_LAYOUT = REL_TYPE.slideLayout;
2192
+ const REL_MASTER = REL_TYPE.slideMaster;
2193
+ const REL_THEME = REL_TYPE.theme;
2194
+ function rootOf(store, partName) {
2195
+ const root = parseXml(store.read(partName)).root;
2196
+ if (root === null) throw new ModelError("MODEL_PART_KIND", `${partName} has no root element`, partName);
2197
+ return root;
2198
+ }
2199
+ function themeFor(state, masterPartName) {
2200
+ const rels = state.store.relationships(masterPartName);
2201
+ const rel = rels.firstOfType(REL_THEME);
2202
+ if (rel === void 0) {
2203
+ state.problems.push({
2204
+ code: "MODEL_NO_THEME",
2205
+ partName: masterPartName,
2206
+ message: "the master has no theme relationship"
2207
+ });
2208
+ return null;
2209
+ }
2210
+ const target = rels.resolve(rel);
2211
+ const cached = state.themes.get(target);
2212
+ if (cached !== void 0) return cached;
2213
+ if (!state.store.has(target)) {
2214
+ state.problems.push({
2215
+ code: "MODEL_PART_MISSING",
2216
+ partName: target,
2217
+ message: `${masterPartName} names a theme part that is not in the package`
2218
+ });
2219
+ return null;
2220
+ }
2221
+ const theme = parseTheme(rootOf(state.store, target), target);
2222
+ state.themes.set(target, theme);
2223
+ return theme;
2224
+ }
2225
+ /**
2226
+ * Load one sheet and, recursively, its parent.
2227
+ *
2228
+ * The `loading` set is a cycle guard rather than an optimisation. A layout whose
2229
+ * `slideMaster` relationship points back at a layout is a package a hostile
2230
+ * input can produce, and following it without a guard is an unbounded recursion
2231
+ * in a Web Worker.
2232
+ */
2233
+ function loadSheet(state, partName) {
2234
+ const cached = state.sheets.get(partName);
2235
+ if (cached !== void 0) return cached;
2236
+ if (state.loading.has(partName)) {
2237
+ state.problems.push({
2238
+ code: "MODEL_PART_KIND",
2239
+ partName,
2240
+ message: "the sheet chain returns to this part"
2241
+ });
2242
+ return null;
2243
+ }
2244
+ if (!state.store.has(partName)) {
2245
+ state.problems.push({
2246
+ code: "MODEL_PART_MISSING",
2247
+ partName,
2248
+ message: "the part is not in the package"
2249
+ });
2250
+ return null;
2251
+ }
2252
+ state.loading.add(partName);
2253
+ let parsed;
2254
+ try {
2255
+ parsed = parseSheet(rootOf(state.store, partName), partName);
2256
+ } catch (error) {
2257
+ state.loading.delete(partName);
2258
+ if (error instanceof ModelError && error.code === "MODEL_PART_KIND") {
2259
+ state.problems.push({
2260
+ code: "MODEL_PART_KIND",
2261
+ partName,
2262
+ message: error.message
2263
+ });
2264
+ return null;
2265
+ }
2266
+ throw error;
2267
+ }
2268
+ let parent = null;
2269
+ let theme = null;
2270
+ if (parsed.kind === "master") theme = themeFor(state, partName);
2271
+ else {
2272
+ const wantedType = parsed.kind === "slide" ? REL_LAYOUT : REL_MASTER;
2273
+ const rels = state.store.relationships(partName);
2274
+ const rel = rels.firstOfType(wantedType);
2275
+ if (rel === void 0) state.problems.push({
2276
+ code: "MODEL_NO_PARENT",
2277
+ partName,
2278
+ message: `no ${parsed.kind === "slide" ? "slideLayout" : "slideMaster"} relationship`
2279
+ });
2280
+ else {
2281
+ parent = loadSheet(state, rels.resolve(rel));
2282
+ if (parent !== null && parent.kind === parsed.kind) {
2283
+ state.problems.push({
2284
+ code: "MODEL_PART_KIND",
2285
+ partName,
2286
+ message: `the parent of a ${parsed.kind} is another ${parsed.kind}`
2287
+ });
2288
+ parent = null;
2289
+ }
2290
+ }
2291
+ }
2292
+ state.loading.delete(partName);
2293
+ const sheet = {
2294
+ ...parsed,
2295
+ parent,
2296
+ theme
2297
+ };
2298
+ state.sheets.set(partName, sheet);
2299
+ return sheet;
2300
+ }
2301
+ function officeDocumentOf(store) {
2302
+ const rel = store.rootRelationships().firstOfType(REL_TYPE.officeDocument);
2303
+ if (rel === void 0) throw new ModelError("MODEL_NO_PRESENTATION", "the package root names no officeDocument");
2304
+ return store.rootRelationships().resolve(rel);
2305
+ }
2306
+ function loadDocument(store) {
2307
+ const presentationPartName = officeDocumentOf(store);
2308
+ if (!store.has(presentationPartName)) throw new ModelError("MODEL_NO_PRESENTATION", `${presentationPartName} is not in the package`, presentationPartName);
2309
+ const presentation = rootOf(store, presentationPartName);
2310
+ const state = {
2311
+ problems: [],
2312
+ sheets: /* @__PURE__ */ new Map(),
2313
+ themes: /* @__PURE__ */ new Map(),
2314
+ loading: /* @__PURE__ */ new Set(),
2315
+ store
2316
+ };
2317
+ const rels = store.relationships(presentationPartName);
2318
+ const slides = [];
2319
+ const slideList = presentation.children.find((child) => child.type === "element" && child.qname === "p:sldIdLst");
2320
+ if (slideList !== void 0 && slideList.type === "element") for (const entry of slideList.children) {
2321
+ if (entry.type !== "element" || entry.qname !== "p:sldId") continue;
2322
+ const id = entry.attributes.find((attr) => attr.qname === "r:id")?.value;
2323
+ if (id === void 0) continue;
2324
+ const target = rels.targetOf(id);
2325
+ if (target === void 0) continue;
2326
+ const sheet = loadSheet(state, target);
2327
+ if (sheet !== null) slides.push(sheet);
2328
+ }
2329
+ for (const rel of rels.byType(REL_SLIDE)) {
2330
+ const target = rels.resolve(rel);
2331
+ if (state.sheets.has(target)) continue;
2332
+ const sheet = loadSheet(state, target);
2333
+ if (sheet !== null) slides.push(sheet);
2334
+ }
2335
+ const masters = [];
2336
+ for (const rel of rels.byType(REL_MASTER)) {
2337
+ const sheet = loadSheet(state, rels.resolve(rel));
2338
+ if (sheet !== null) masters.push(sheet);
2339
+ }
2340
+ const layouts = [];
2341
+ for (const master of masters) {
2342
+ const masterRels = store.relationships(master.partName);
2343
+ for (const rel of masterRels.byType(REL_LAYOUT)) {
2344
+ const sheet = loadSheet(state, masterRels.resolve(rel));
2345
+ if (sheet !== null && !layouts.includes(sheet)) layouts.push(sheet);
2346
+ }
2347
+ }
2348
+ const size = presentation.children.find((child) => child.type === "element" && child.qname === "p:sldSz");
2349
+ const dimension = (name) => {
2350
+ if (size === void 0 || size.type !== "element") return 0;
2351
+ const raw = size.attributes.find((attr) => attr.qname === name)?.value;
2352
+ return raw === void 0 ? 0 : Number(raw);
2353
+ };
2354
+ const sheets = state.sheets;
2355
+ return {
2356
+ presentation,
2357
+ presentationPartName,
2358
+ slides,
2359
+ masters,
2360
+ layouts,
2361
+ themes: [...state.themes.values()],
2362
+ problems: state.problems,
2363
+ defaultTextStyle: parseDefaultTextStyle(presentation, presentationPartName),
2364
+ slideSize: {
2365
+ cx: dimension("cx"),
2366
+ cy: dimension("cy")
2367
+ },
2368
+ sheet(partName) {
2369
+ return sheets.get(partName);
2370
+ }
2371
+ };
2372
+ }
2373
+ //#endregion
2374
+ //#region src/builtin-text-styles.ts
2375
+ /** The three buckets, nine levels each, level 1 first. */
2376
+ const BUILTIN_TEXT_STYLES = {
2377
+ title: [
2378
+ {
2379
+ sz: 4400,
2380
+ marL: 0,
2381
+ indent: 0,
2382
+ bullet: false,
2383
+ typeface: "+mj-lt"
2384
+ },
2385
+ {
2386
+ sz: 1800,
2387
+ marL: 0,
2388
+ indent: 0,
2389
+ bullet: false,
2390
+ typeface: null
2391
+ },
2392
+ {
2393
+ sz: 1800,
2394
+ marL: 0,
2395
+ indent: 0,
2396
+ bullet: false,
2397
+ typeface: null
2398
+ },
2399
+ {
2400
+ sz: 1800,
2401
+ marL: 0,
2402
+ indent: 0,
2403
+ bullet: false,
2404
+ typeface: null
2405
+ },
2406
+ {
2407
+ sz: 1800,
2408
+ marL: 0,
2409
+ indent: 0,
2410
+ bullet: false,
2411
+ typeface: null
2412
+ },
2413
+ {
2414
+ sz: 1800,
2415
+ marL: 0,
2416
+ indent: 0,
2417
+ bullet: false,
2418
+ typeface: null
2419
+ },
2420
+ {
2421
+ sz: 1800,
2422
+ marL: 0,
2423
+ indent: 0,
2424
+ bullet: false,
2425
+ typeface: null
2426
+ },
2427
+ {
2428
+ sz: 1800,
2429
+ marL: 0,
2430
+ indent: 0,
2431
+ bullet: false,
2432
+ typeface: null
2433
+ },
2434
+ {
2435
+ sz: 1800,
2436
+ marL: 0,
2437
+ indent: 0,
2438
+ bullet: false,
2439
+ typeface: null
2440
+ }
2441
+ ],
2442
+ body: [
2443
+ {
2444
+ sz: 2800,
2445
+ marL: 228600,
2446
+ indent: -228600,
2447
+ bullet: true,
2448
+ typeface: "+mn-lt"
2449
+ },
2450
+ {
2451
+ sz: 2400,
2452
+ marL: 685800,
2453
+ indent: -228600,
2454
+ bullet: true,
2455
+ typeface: "+mn-lt"
2456
+ },
2457
+ {
2458
+ sz: 2e3,
2459
+ marL: 1143e3,
2460
+ indent: -228600,
2461
+ bullet: true,
2462
+ typeface: "+mn-lt"
2463
+ },
2464
+ {
2465
+ sz: 1800,
2466
+ marL: 1600200,
2467
+ indent: -228600,
2468
+ bullet: true,
2469
+ typeface: "+mn-lt"
2470
+ },
2471
+ {
2472
+ sz: 1800,
2473
+ marL: 2057400,
2474
+ indent: -228600,
2475
+ bullet: true,
2476
+ typeface: "+mn-lt"
2477
+ },
2478
+ {
2479
+ sz: 1800,
2480
+ marL: 2514600,
2481
+ indent: -228600,
2482
+ bullet: true,
2483
+ typeface: "+mn-lt"
2484
+ },
2485
+ {
2486
+ sz: 1800,
2487
+ marL: 2971800,
2488
+ indent: -228600,
2489
+ bullet: true,
2490
+ typeface: "+mn-lt"
2491
+ },
2492
+ {
2493
+ sz: 1800,
2494
+ marL: 3429e3,
2495
+ indent: -228600,
2496
+ bullet: true,
2497
+ typeface: "+mn-lt"
2498
+ },
2499
+ {
2500
+ sz: 1800,
2501
+ marL: 3886200,
2502
+ indent: -228600,
2503
+ bullet: true,
2504
+ typeface: "+mn-lt"
2505
+ }
2506
+ ],
2507
+ other: [
2508
+ {
2509
+ sz: 1800,
2510
+ marL: 0,
2511
+ indent: 0,
2512
+ bullet: false,
2513
+ typeface: "+mn-lt"
2514
+ },
2515
+ {
2516
+ sz: 1800,
2517
+ marL: 457200,
2518
+ indent: 0,
2519
+ bullet: false,
2520
+ typeface: "+mn-lt"
2521
+ },
2522
+ {
2523
+ sz: 1800,
2524
+ marL: 914400,
2525
+ indent: 0,
2526
+ bullet: false,
2527
+ typeface: "+mn-lt"
2528
+ },
2529
+ {
2530
+ sz: 1800,
2531
+ marL: 1371600,
2532
+ indent: 0,
2533
+ bullet: false,
2534
+ typeface: "+mn-lt"
2535
+ },
2536
+ {
2537
+ sz: 1800,
2538
+ marL: 1828800,
2539
+ indent: 0,
2540
+ bullet: false,
2541
+ typeface: "+mn-lt"
2542
+ },
2543
+ {
2544
+ sz: 1800,
2545
+ marL: 2286e3,
2546
+ indent: 0,
2547
+ bullet: false,
2548
+ typeface: "+mn-lt"
2549
+ },
2550
+ {
2551
+ sz: 1800,
2552
+ marL: 2743200,
2553
+ indent: 0,
2554
+ bullet: false,
2555
+ typeface: "+mn-lt"
2556
+ },
2557
+ {
2558
+ sz: 1800,
2559
+ marL: 3200400,
2560
+ indent: 0,
2561
+ bullet: false,
2562
+ typeface: "+mn-lt"
2563
+ },
2564
+ {
2565
+ sz: 1800,
2566
+ marL: 3657600,
2567
+ indent: 0,
2568
+ bullet: false,
2569
+ typeface: "+mn-lt"
2570
+ }
2571
+ ]
2572
+ };
2573
+ /**
2574
+ * What is left when every source is silent and the master *does* declare
2575
+ * `p:txStyles`.
2576
+ *
2577
+ * Distinct from the table above, and measured separately: a body placeholder
2578
+ * whose `p:bodyStyle/a:lvl1pPr` declares only a `marL` came back at
2579
+ * 18 points rather than the built-in body style's
2580
+ * 28, and with no indent rather than the built-in's
2581
+ * hanging one. So the built-in styles are substituted wholesale for an absent
2582
+ * `p:txStyles` and are not a per-property backstop underneath a present one.
2583
+ *
2584
+ * `marL` and `indent` are **0**, not the ECMA-376 schema defaults of 347663 and
2585
+ * -342900. Materialising those at parse time gives every paragraph a
2586
+ * 27-point hanging indent nothing asked for, and blocks the inherited value on
2587
+ * top of it, because a default that has been written down cannot be told from a
2588
+ * declaration.
2589
+ *
2590
+ * The typeface is the minor face on all 6 probes that reach a silent cascade.
2591
+ */
2592
+ const TEXT_FLOOR = {
2593
+ sz: 1800,
2594
+ marL: 0,
2595
+ indent: 0,
2596
+ bullet: false,
2597
+ typeface: "+mn-lt"
2598
+ };
2599
+ //#endregion
2600
+ //#region src/resolve/typeface.ts
2601
+ /**
2602
+ * `+mn-lt` and its five siblings, resolved against a theme's `a:fontScheme`.
2603
+ *
2604
+ * Measured 8 of 8 against a reading that swaps the two collections, which fits
2605
+ * 2. ADR 0033.
2606
+ */
2607
+ /**
2608
+ * The typeface a slot asks for, with any theme reference followed.
2609
+ *
2610
+ * `undefined` means the reference resolved to a collection entry the theme
2611
+ * leaves empty, which is not the same as the theme naming nothing.
2612
+ */
2613
+ function resolveTypeface(typeface, scheme) {
2614
+ const ref = themeFontRef(typeface);
2615
+ if (ref === null) return typeface;
2616
+ const name = (ref.collection === "major" ? scheme.major : scheme.minor)[ref.script];
2617
+ if (name === null || name.length === 0) return void 0;
2618
+ return name;
2619
+ }
2620
+ /** The same, for a whole `CT_TextFont`. */
2621
+ function resolveTypefaceOf(font, scheme) {
2622
+ return resolveTypeface(font.typeface, scheme);
2623
+ }
2624
+ /**
2625
+ * Every distinct typeface a set of runs asks for, theme references followed.
2626
+ *
2627
+ * This is what `fontReport` is given: a report keyed on `+mn-lt` would name a
2628
+ * token no user recognises and would collapse two themes into one row.
2629
+ */
2630
+ function requestedTypefaces(fonts, scheme) {
2631
+ const out = [];
2632
+ for (const font of fonts) {
2633
+ if (font === void 0) continue;
2634
+ const name = resolveTypeface(font.typeface, scheme);
2635
+ if (name !== void 0) out.push(name);
2636
+ }
2637
+ return out;
2638
+ }
2639
+ //#endregion
2640
+ //#region src/resolve/text.ts
2641
+ /**
2642
+ * The text cascade: one walk, two termini, and the origin of every answer.
2643
+ *
2644
+ * `resolveRun(context, paragraph, run, pick)` returns the first level that
2645
+ * declared the property asked for **and which level that was**, exactly as
2646
+ * `resolve` does for geometry and fills. The inspector's provenance chips,
2647
+ * per-property Reset, and knowing what a theme swap invalidates all fall out of
2648
+ * the same field, read by the same function the renderer reads.
2649
+ *
2650
+ * ## The walk
2651
+ *
2652
+ * 1. the run's own `a:rPr`
2653
+ * 2. the paragraph's `a:pPr/a:defRPr`
2654
+ * 3. the shape's `a:lstStyle`, at the paragraph's level
2655
+ * 4. each ancestor placeholder's `a:lstStyle`, up the chain 2.9 measured for
2656
+ * geometry: `@idx` alone to the layout, the folded `@type` to the master
2657
+ *
2658
+ * ## The two termini, which are not one ladder
2659
+ *
2660
+ * A shape whose chain ends at a type with a bucket - `title` and `ctrTitle` to
2661
+ * `p:titleStyle`, the content types to `p:bodyStyle` - reads that bucket and
2662
+ * **stops**. A shape with no bucket reads `p:defaultTextStyle` and **stops**.
2663
+ *
2664
+ * Measured in 3.1, and it is where every implementation this project has read
2665
+ * goes wrong. Scored over the eight combinations of the three choices anyone
2666
+ * could make, this reading fits 37 of 37 probes and the reading the plan
2667
+ * specified - the shape's own `@type` picks the bucket, the header-and-footer
2668
+ * trio reads `p:otherStyle`, and so does a shape that is not a placeholder -
2669
+ * fits 13. Three of the four disagreements are visible on any deck:
2670
+ *
2671
+ * - **The `@type` at the end of the chain picks the bucket, not the shape's.**
2672
+ * The first hop matches on `@idx` alone, so a slide `title` at idx 0 really
2673
+ * does land on a layout `body` at idx 0, and when it does it reads
2674
+ * `p:bodyStyle`. Measured both ways round and twice more through a `sldNum`.
2675
+ * - **`dt`, `ftr`, `sldNum` and `hdr` reach no bucket at all** and fall through
2676
+ * to `p:defaultTextStyle`, which a body placeholder ignores even when it is
2677
+ * there in the same package.
2678
+ * - **Nothing measurable reads `p:otherStyle`** - not those four, not a shape
2679
+ * that is not a placeholder, on the slide, the layout or the master.
2680
+ *
2681
+ * ## Every level contributes only what it declares
2682
+ *
2683
+ * The walk is per property, not per level. Measured with five levels declaring
2684
+ * one property each: the run came back 24-point bold italic underlined and
2685
+ * struck through, where a nearest-level-takes-all reading gives an 18-point run
2686
+ * that is only struck through.
2687
+ */
2688
+ /** `title` and `ctrTitle`. Everything else with a bucket has the body one. */
2689
+ const TITLE_TYPES = /* @__PURE__ */ new Set(["title", "ctrTitle"]);
2690
+ /**
2691
+ * The four types that reach no bucket.
2692
+ *
2693
+ * Measured, and against the plan, which assigned all four to `p:otherStyle`. A
2694
+ * `sldNum` placeholder in a package declaring `p:otherStyle` at 12 points and
2695
+ * `p:defaultTextStyle` at 20 came back at 20.
2696
+ */
2697
+ const NO_BUCKET_TYPES = /* @__PURE__ */ new Set([
2698
+ "dt",
2699
+ "ftr",
2700
+ "sldNum",
2701
+ "hdr"
2702
+ ]);
2703
+ /** Which `p:txStyles` bucket a placeholder type reads, or `null` for none. */
2704
+ function bucketOf(type) {
2705
+ if (TITLE_TYPES.has(type)) return "title";
2706
+ if (NO_BUCKET_TYPES.has(type)) return null;
2707
+ return "body";
2708
+ }
2709
+ function terminusOf(context) {
2710
+ const placeholder = inheritanceChain(context.shape, context.sheet).at(-1)?.shape.placeholder ?? null;
2711
+ let master = null;
2712
+ for (let sheet = context.sheet; sheet !== null; sheet = sheet.parent) if (sheet.kind === "master") {
2713
+ master = sheet;
2714
+ break;
2715
+ }
2716
+ return {
2717
+ bucket: placeholder === null ? null : bucketOf(normalizePlaceholder(placeholder).type),
2718
+ master
2719
+ };
2720
+ }
2721
+ /**
2722
+ * The list styles a shape reads, nearest first, ending at whichever terminus it
2723
+ * reaches.
2724
+ *
2725
+ * Exported because it *is* the finding: a caller that wants to show the user
2726
+ * where a value could have come from - the inspector's Overrides panel, layout
2727
+ * compatibility scoring - needs the same list the resolver walks, not a second
2728
+ * opinion about it.
2729
+ */
2730
+ function textLevels(context) {
2731
+ const levels = inheritanceChain(context.shape, context.sheet).map((link, index) => ({
2732
+ style: link.shape.text?.lstStyle,
2733
+ origin: index === 0 ? "shape" : link.origin,
2734
+ sheet: link.sheet,
2735
+ shape: link.shape
2736
+ }));
2737
+ const { bucket, master } = terminusOf(context);
2738
+ if (bucket === null) {
2739
+ levels.push({
2740
+ style: context.defaultTextStyle,
2741
+ origin: "defaultTextStyle",
2742
+ sheet: null,
2743
+ shape: null
2744
+ });
2745
+ return levels;
2746
+ }
2747
+ levels.push({
2748
+ style: master?.txStyles?.[bucket],
2749
+ origin: "txStyles",
2750
+ sheet: master,
2751
+ shape: null
2752
+ });
2753
+ return levels;
2754
+ }
2755
+ /**
2756
+ * The values left when every level has been asked and none of them said.
2757
+ *
2758
+ * Two quite different things, and which applies turns on one question: does the
2759
+ * master declare `p:txStyles` at all? If it does not, PowerPoint substitutes its
2760
+ * whole built-in set, and a body placeholder is 28 points with a hanging bullet
2761
+ * indent. If it does, there is no per-property backstop underneath it and what
2762
+ * remains is the floor - 18 points, no margin, no indent. Measured as two
2763
+ * separate probes precisely because the two are easy to conflate and differ on
2764
+ * every partially-specified master.
2765
+ */
2766
+ function floorOf(context, level) {
2767
+ const { bucket, master } = terminusOf(context);
2768
+ if (master?.txStyles !== void 0) return TEXT_FLOOR;
2769
+ return BUILTIN_TEXT_STYLES[bucket ?? "other"][clampLevel(level)] ?? TEXT_FLOOR;
2770
+ }
2771
+ function clampLevel(level) {
2772
+ if (!Number.isInteger(level)) throw new ModelError("MODEL_TEXT_LEVEL", `paragraph level ${String(level)} is not an integer`);
2773
+ return Math.max(0, Math.min(8, level));
2774
+ }
2775
+ function resolvedAt(value, level, explicit) {
2776
+ return {
2777
+ value,
2778
+ origin: level.origin,
2779
+ explicit,
2780
+ sheet: level.sheet,
2781
+ shape: level.shape
2782
+ };
2783
+ }
2784
+ /**
2785
+ * A paragraph property, from the paragraph's own `a:pPr` or up the walk.
2786
+ *
2787
+ * `pick` returns `undefined` for "this level did not say" and anything else for
2788
+ * a value, including `null` - the same contract `resolve` uses, and for the same
2789
+ * reason: a level may legitimately declare a `null`, and conflating that with
2790
+ * silence is how an explicit value gets overwritten by an inherited one.
2791
+ */
2792
+ function resolveParagraph(context, paragraph, pick) {
2793
+ if (paragraph.props !== void 0) {
2794
+ const own = pick(paragraph.props);
2795
+ if (own !== void 0) return {
2796
+ value: own,
2797
+ origin: "paragraph",
2798
+ explicit: true,
2799
+ sheet: context.sheet,
2800
+ shape: context.shape
2801
+ };
2802
+ }
2803
+ const level = clampLevel(paragraph.level);
2804
+ for (const entry of textLevels(context)) {
2805
+ const props = entry.style?.levels[level];
2806
+ if (props === void 0) continue;
2807
+ const value = pick(props);
2808
+ if (value !== void 0) return resolvedAt(value, entry, entry.origin === "shape");
2809
+ }
2810
+ }
2811
+ /**
2812
+ * A run property, from the run's own `a:rPr` and then every `a:defRPr` above it.
2813
+ *
2814
+ * The paragraph's `a:pPr/a:defRPr` is one level, not a special case: it sits
2815
+ * between the run and the shape's `a:lstStyle`, which the ladder pinned by
2816
+ * declaring a different size at each and reading back which one won.
2817
+ */
2818
+ function resolveRun(context, paragraph, run, pick) {
2819
+ if (run?.props !== void 0) {
2820
+ const own = pick(run.props);
2821
+ if (own !== void 0) return {
2822
+ value: own,
2823
+ origin: "run",
2824
+ explicit: true,
2825
+ sheet: context.sheet,
2826
+ shape: context.shape
2827
+ };
2828
+ }
2829
+ if (paragraph.props?.defRPr !== void 0) {
2830
+ const value = pick(paragraph.props.defRPr);
2831
+ if (value !== void 0) return {
2832
+ value,
2833
+ origin: "paragraph",
2834
+ explicit: true,
2835
+ sheet: context.sheet,
2836
+ shape: context.shape
2837
+ };
2838
+ }
2839
+ const level = clampLevel(paragraph.level);
2840
+ for (const entry of textLevels(context)) {
2841
+ const defRPr = entry.style?.levels[level]?.defRPr;
2842
+ if (defRPr === void 0) continue;
2843
+ const value = pick(defRPr);
2844
+ if (value !== void 0) return resolvedAt(value, entry, entry.origin === "shape");
2845
+ }
2846
+ }
2847
+ /**
2848
+ * The font size of a run, in hundredths of a point, always.
2849
+ *
2850
+ * The only resolver here that cannot return `undefined`: text has to be drawn at
2851
+ * some size, and the size when nothing declared one is measured rather than
2852
+ * chosen. `origin` is `builtin` when the master declares no `p:txStyles` and
2853
+ * `schemaDefault` when it does and the bucket said nothing - two different
2854
+ * numbers on the same slide, and the field says which was used.
2855
+ */
2856
+ function resolveSize(context, paragraph, run) {
2857
+ const found = resolveRun(context, paragraph, run, (props) => props.sz);
2858
+ if (found !== void 0) return found;
2859
+ return floorResolved(context, paragraph, (level) => level.sz);
2860
+ }
2861
+ /**
2862
+ * The Latin typeface of a run, with any theme reference followed.
2863
+ *
2864
+ * The second resolver that cannot return `undefined`: a run has to be drawn in
2865
+ * some face, and the face when nothing named one is measured rather than
2866
+ * chosen - the theme's minor, on all six probes reaching a silent cascade.
2867
+ */
2868
+ function resolveLatinTypeface(context, paragraph, run) {
2869
+ const scheme = themeOf(context.sheet)?.fonts ?? null;
2870
+ /** A name a source gave, with a theme reference followed where one can be. */
2871
+ const faceOf = (typeface) => {
2872
+ if (themeFontRef(typeface) === null) return typeface;
2873
+ return scheme === null ? void 0 : resolveTypeface(typeface, scheme);
2874
+ };
2875
+ const named = resolveRun(context, paragraph, run, (props) => props.latin);
2876
+ if (named !== void 0) {
2877
+ const face = faceOf(named.value.typeface);
2878
+ if (face !== void 0) return {
2879
+ ...named,
2880
+ value: face
2881
+ };
2882
+ }
2883
+ const floor = floorOf(context, paragraph.level);
2884
+ const reference = floor.typeface ?? TEXT_FLOOR.typeface;
2885
+ const face = reference === null ? void 0 : faceOf(reference);
2886
+ if (face === void 0) throw new ModelError("MODEL_TEXT_TYPEFACE", "no source in the cascade names a typeface, and the theme names none either");
2887
+ return {
2888
+ value: face,
2889
+ origin: floor === TEXT_FLOOR ? "schemaDefault" : "builtin",
2890
+ explicit: false,
2891
+ sheet: null,
2892
+ shape: null
2893
+ };
2894
+ }
2895
+ /** `@marL` in EMU. Measured to be 0 when nothing declares it, not 347663. */
2896
+ function resolveMarginLeft(context, paragraph) {
2897
+ const found = resolveParagraph(context, paragraph, (props) => props.marL);
2898
+ if (found !== void 0) return found;
2899
+ return floorResolved(context, paragraph, (level) => level.marL);
2900
+ }
2901
+ /** `@indent` in EMU. Measured to be 0 when nothing declares it, not -342900. */
2902
+ function resolveIndent(context, paragraph) {
2903
+ const found = resolveParagraph(context, paragraph, (props) => props.indent);
2904
+ if (found !== void 0) return found;
2905
+ return floorResolved(context, paragraph, (level) => level.indent);
2906
+ }
2907
+ /**
2908
+ * The bullet, merged the way T5 measured it: five slots, each on its own.
2909
+ *
2910
+ * The kind is one slot because the schema makes the four elements exclusive, and
2911
+ * the three decorations are three more because PowerPoint merges them
2912
+ * separately - a level declaring only `a:buFont` re-faces the character it
2913
+ * inherits and keeps its size and colour. Measured on eleven cases twice over,
2914
+ * once through a shape's own `a:lstStyle` and once through three hops of the
2915
+ * master chain, and the two agree.
2916
+ *
2917
+ * Every one of these returns `undefined` when nothing in the chain declares the
2918
+ * slot, which for the kind is the difference between "no bullet" and "nobody
2919
+ * said" - `a:buNone` resolves to `'none'` and an empty chain to `undefined`.
2920
+ * Nothing here supplies a default, because a paragraph with no bullet anywhere
2921
+ * in its chain genuinely has none.
2922
+ */
2923
+ function resolveBulletKind(context, paragraph) {
2924
+ return resolveParagraph(context, paragraph, (props) => props.buKind);
2925
+ }
2926
+ /** `a:buChar/@char`, as written. The symbol mapping belongs to the renderer. */
2927
+ function resolveBulletChar(context, paragraph) {
2928
+ return resolveParagraph(context, paragraph, (props) => props.buChar);
2929
+ }
2930
+ /** `a:buAutoNum`, type and start value together, since one is meaningless alone. */
2931
+ function resolveBulletAutoNum(context, paragraph) {
2932
+ return resolveParagraph(context, paragraph, (props) => props.buAutoNum);
2933
+ }
2934
+ /** `a:buBlip/a:blip/@r:embed`, which the caller resolves against the part's rels. */
2935
+ function resolveBulletBlip(context, paragraph) {
2936
+ return resolveParagraph(context, paragraph, (props) => props.buBlip);
2937
+ }
2938
+ /**
2939
+ * `a:buFont` or `a:buFontTx`.
2940
+ *
2941
+ * Worth knowing before using it: this is **inert for an autonumber**. PowerPoint
2942
+ * draws a number in the first run's face whatever `a:buFont` says - measured on
2943
+ * twenty probes, including one where `buFont` named Wingdings and the run named
2944
+ * Courier New - and its own UI writes `buFont="+mj-lt"` on every list it
2945
+ * numbers. The resolver still reports it, because the file says it and 1.3 has
2946
+ * to write it back; the renderer is where it is ignored.
2947
+ */
2948
+ function resolveBulletFont(context, paragraph) {
2949
+ return resolveParagraph(context, paragraph, (props) => props.buFont);
2950
+ }
2951
+ /** `a:buSzPct`, `a:buSzPts` or `a:buSzTx`; a percentage is of the first run's size. */
2952
+ function resolveBulletSize(context, paragraph) {
2953
+ return resolveParagraph(context, paragraph, (props) => props.buSize);
2954
+ }
2955
+ /** `a:buClr` or `a:buClrTx`. Unlike the font, this one *does* reach an autonumber. */
2956
+ function resolveBulletColor(context, paragraph) {
2957
+ return resolveParagraph(context, paragraph, (props) => props.buColor);
2958
+ }
2959
+ function floorResolved(context, paragraph, pick) {
2960
+ const floor = floorOf(context, paragraph.level);
2961
+ return {
2962
+ value: pick(floor),
2963
+ origin: floor === TEXT_FLOOR ? "schemaDefault" : "builtin",
2964
+ explicit: false,
2965
+ sheet: null,
2966
+ shape: null
2967
+ };
2968
+ }
2969
+ //#endregion
2970
+ //#region src/resolve/body.ts
2971
+ /**
2972
+ * `a:bodyPr`, resolved up the placeholder chain.
2973
+ *
2974
+ * Every attribute inherits on its own, from any level: a bare `<a:bodyPr/>` on a
2975
+ * slide is anchored where its master says, not at the schema default. ADR 0032.
2976
+ */
2977
+ const EMU_PER_POINT = 12700;
2978
+ /** Points. Asymmetric, and each edge defaults on its own. */
2979
+ const DEFAULT_INSETS = {
2980
+ left: 7.2,
2981
+ top: 3.6,
2982
+ right: 7.2,
2983
+ bottom: 3.6
2984
+ };
2985
+ /**
2986
+ * One property of the text frame, from the shape's own `a:bodyPr` or above it.
2987
+ *
2988
+ * `pick` returns `undefined` for "this level did not say", the same contract the
2989
+ * text cascade uses.
2990
+ */
2991
+ function resolveBody(shape, sheet, pick) {
2992
+ for (const [index, link] of inheritanceChain(shape, sheet).entries()) {
2993
+ const props = link.shape.text?.bodyPr;
2994
+ if (props === void 0) continue;
2995
+ const value = pick(props);
2996
+ if (value === void 0) continue;
2997
+ return {
2998
+ value,
2999
+ origin: index === 0 ? "shape" : link.origin,
3000
+ explicit: index === 0,
3001
+ sheet: link.sheet,
3002
+ shape: link.shape
3003
+ };
3004
+ }
3005
+ }
3006
+ function resolveAnchor(shape, sheet) {
3007
+ return resolveBody(shape, sheet, (p) => p.anchor);
3008
+ }
3009
+ function resolveAnchorCtr(shape, sheet) {
3010
+ return resolveBody(shape, sheet, (p) => p.anchorCtr);
3011
+ }
3012
+ function resolveVertical(shape, sheet) {
3013
+ return resolveBody(shape, sheet, (p) => p.vert);
3014
+ }
3015
+ function resolveWrap(shape, sheet) {
3016
+ return resolveBody(shape, sheet, (p) => p.wrap);
3017
+ }
3018
+ function resolveColumns(shape, sheet) {
3019
+ return resolveBody(shape, sheet, (p) => p.numCol);
3020
+ }
3021
+ /**
3022
+ * The four insets, in points, each falling back on its own.
3023
+ *
3024
+ * A frame stating only `lIns` still gets 3.6 above and below, so this resolves
3025
+ * four times rather than once.
3026
+ */
3027
+ function resolveInsets(shape, sheet) {
3028
+ const at = (pick, fallback) => {
3029
+ const found = resolveBody(shape, sheet, pick);
3030
+ return found === void 0 ? fallback : found.value / EMU_PER_POINT;
3031
+ };
3032
+ return {
3033
+ left: at((p) => p.lIns, DEFAULT_INSETS.left),
3034
+ top: at((p) => p.tIns, DEFAULT_INSETS.top),
3035
+ right: at((p) => p.rIns, DEFAULT_INSETS.right),
3036
+ bottom: at((p) => p.bIns, DEFAULT_INSETS.bottom)
3037
+ };
3038
+ }
3039
+ //#endregion
3040
+ export { BUILTIN_TEXT_STYLES, COLOR_TRANSFORM_OPS, DEFAULT_PLACEHOLDER_IDX, DEFAULT_PLACEHOLDER_TYPE, LEVELS, MASTER_PLACEHOLDER_TYPES, MODEL_ERROR_CODES, ModelError, ORPHAN_RECT, PLACEHOLDER_TYPES, STYLE_MATRIX_OFFSET, TEXT_FLOOR, backgroundSheet, bucketOf, colorContextOf, colorMapOf, floorOf, inheritanceChain, isModelError, loadDocument, masterOf, masterPlaceholderType, matchInLayout, matchInMaster, normalizePlaceholder, parseBodyProps, parseBodyPropsChild, parseClrMap, parseColorChild, parseColorElement, parseDefaultTextStyle, parseEffects, parseFill, parseFillElement, parseGeometry, parseLine, parseLineElement, parseListStyle, parseParaProps, parseRunProps, parseSheet, parseTextBody, parseTextBodyChild, parseTextStyles, parseTheme, phClrOf, placeholders, requestedTypefaces, resolve, resolveAnchor, resolveAnchorCtr, resolveAppearance, resolveBackground, resolveBackgroundColor, resolveBody, resolveBulletAutoNum, resolveBulletBlip, resolveBulletChar, resolveBulletColor, resolveBulletFont, resolveBulletKind, resolveBulletSize, resolveColumns, resolveIndent, resolveInsets, resolveLatinTypeface, resolveMarginLeft, resolveOnSheet, resolveParagraph, resolveRun, resolveSize, resolveSolidFill, resolveTypeface, resolveTypefaceOf, resolveVertical, resolveWrap, resolveXfrm, schemeOf, sheetChain, styleMatrixEffects, styleMatrixFill, styleMatrixLine, styleMatrixTarget, textLevels, themeFontRef, themeOf };
3041
+
3042
+ //# sourceMappingURL=index.js.map