@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/CHANGELOG.md +25 -0
- package/LICENSE +202 -0
- package/NOTICE +43 -0
- package/README.md +183 -0
- package/dist/index.d.ts +1340 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3042 -0
- package/dist/index.js.map +1 -0
- package/package.json +59 -0
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
|