@shbernal/ts-pptx 1.0.0 → 3.0.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.
Files changed (72) hide show
  1. package/README.md +10 -3
  2. package/dist/{autopage-tmfjRzL4.js → autopage-MxulehqQ.js} +3 -4
  3. package/dist/{autopage-tmfjRzL4.js.map → autopage-MxulehqQ.js.map} +1 -1
  4. package/dist/browser.d.ts +14 -6
  5. package/dist/browser.d.ts.map +1 -1
  6. package/dist/browser.js +12 -4
  7. package/dist/browser.js.map +1 -1
  8. package/dist/{diagnostics-DzY9Tyfm.d.ts → diagnostics-BqBTmA3a.d.ts} +95 -23
  9. package/dist/diagnostics-BqBTmA3a.d.ts.map +1 -0
  10. package/dist/{enums-D3aF4QGb.d.ts → enums-oLLeaSW3.d.ts} +2 -1
  11. package/dist/{enums-D3aF4QGb.d.ts.map → enums-oLLeaSW3.d.ts.map} +1 -1
  12. package/dist/{errors-DRLpOpgQ.d.ts → errors-BENA-Xuw.d.ts} +3 -3
  13. package/dist/{errors-DRLpOpgQ.d.ts.map → errors-BENA-Xuw.d.ts.map} +1 -1
  14. package/dist/errors-DGE9zh3U.js.map +1 -1
  15. package/dist/{fit-CbxyHN69.js → fit-DGSwMMNs.js} +0 -0
  16. package/dist/{fit-CbxyHN69.js.map → fit-DGSwMMNs.js.map} +1 -1
  17. package/dist/{html-dom-wVa3TiG7.js → html-dom--J-Xuucf.js} +19 -5
  18. package/dist/html-dom--J-Xuucf.js.map +1 -0
  19. package/dist/html.d.ts +3 -3
  20. package/dist/html.d.ts.map +1 -1
  21. package/dist/html.js +6 -3
  22. package/dist/html.js.map +1 -1
  23. package/dist/index.d.ts +6 -6
  24. package/dist/index.js +4 -4
  25. package/dist/index.js.map +1 -1
  26. package/dist/inspect.d.ts +1 -1
  27. package/dist/inspect.js +2 -2
  28. package/dist/inspect.js.map +1 -1
  29. package/dist/math.d.ts +1 -1
  30. package/dist/math.js +2 -1
  31. package/dist/math.js.map +1 -1
  32. package/dist/measure.d.ts +2 -2
  33. package/dist/measure.js +1 -1
  34. package/dist/node.d.ts +14 -6
  35. package/dist/node.d.ts.map +1 -1
  36. package/dist/node.js +11 -3
  37. package/dist/node.js.map +1 -1
  38. package/dist/{pres-5x1bjCzk.d.ts → pres-2WsBGimU.d.ts} +3 -3
  39. package/dist/{pres-5x1bjCzk.d.ts.map → pres-2WsBGimU.d.ts.map} +1 -1
  40. package/dist/{presentation-RGOiun5P.d.ts → presentation-CTVPpiwP.d.ts} +912 -485
  41. package/dist/presentation-CTVPpiwP.d.ts.map +1 -0
  42. package/dist/{presentation-B0JWdTN1.js → presentation-DAcOfzsP.js} +880 -366
  43. package/dist/presentation-DAcOfzsP.js.map +1 -0
  44. package/dist/read.d.ts +4 -4
  45. package/dist/read.d.ts.map +1 -1
  46. package/dist/read.js +2 -2
  47. package/dist/script.d.ts +2 -2
  48. package/dist/script.d.ts.map +1 -1
  49. package/dist/script.js +0 -0
  50. package/dist/script.js.map +1 -1
  51. package/dist/{shapes-BZU8UTW2.js → shapes-BWifcHDV.js} +1520 -996
  52. package/dist/shapes-BWifcHDV.js.map +1 -0
  53. package/dist/{slide-B6mDaM-P.d.ts → slide-YuSCGa45.d.ts} +409 -103
  54. package/dist/slide-YuSCGa45.d.ts.map +1 -0
  55. package/dist/{image-size-C5Z1sKof.js → st-enums-CgadEyPM.js} +235 -4
  56. package/dist/st-enums-CgadEyPM.js.map +1 -0
  57. package/dist/{text-WRUbOy2r.js → text-120VFSDm.js} +1027 -369
  58. package/dist/text-120VFSDm.js.map +1 -0
  59. package/dist/{units-internal-7JcMfYti.js → units-internal-CiFWZWk4.js} +2 -1
  60. package/dist/units-internal-CiFWZWk4.js.map +1 -0
  61. package/dist/zip.d.ts +2 -2
  62. package/dist/zip.js.map +1 -1
  63. package/package.json +38 -36
  64. package/dist/diagnostics-DzY9Tyfm.d.ts.map +0 -1
  65. package/dist/html-dom-wVa3TiG7.js.map +0 -1
  66. package/dist/image-size-C5Z1sKof.js.map +0 -1
  67. package/dist/presentation-B0JWdTN1.js.map +0 -1
  68. package/dist/presentation-RGOiun5P.d.ts.map +0 -1
  69. package/dist/shapes-BZU8UTW2.js.map +0 -1
  70. package/dist/slide-B6mDaM-P.d.ts.map +0 -1
  71. package/dist/text-WRUbOy2r.js.map +0 -1
  72. package/dist/units-internal-7JcMfYti.js.map +0 -1
@@ -1,6 +1,6 @@
1
1
  import { n as warn } from "./diagnostics-DYQC7xLV.js";
2
2
  import { i as PackageReadError, n as InvalidOptionError, o as UnsupportedFeatureError, t as InternalError } from "./errors-DGE9zh3U.js";
3
- import { r as getImageSizeFromBytes, t as fitSrcRectPercents } from "./image-size-C5Z1sKof.js";
3
+ import { C as getImageSizeFromBytes, _ as SLIDE_LAYOUT_REL, b as THEME_REL, c as TEXT_VERTICAL, d as IMAGE_REL, f as NOTES_MASTER_REL, l as checkEnumOrThrow, m as OFFICE_DOCUMENT_REL, o as TEXT_ANCHORS, s as TEXT_HORZ_OVERFLOW, v as SLIDE_MASTER_REL, x as fitSrcRectPercents } from "./st-enums-CgadEyPM.js";
4
4
  import { DOMParser, MIME_TYPE, XMLSerializer, onErrorStopParsing } from "@xmldom/xmldom";
5
5
  /** Parse an XML string strictly: any well-formedness error throws. */
6
6
  function parseXml(xml) {
@@ -58,11 +58,25 @@ function getElements(parent, qname) {
58
58
  return out;
59
59
  }
60
60
  /** All direct child elements in document order, regardless of qname. */
61
- function childElements$2(parent) {
61
+ function childElements(parent) {
62
62
  const out = [];
63
63
  for (let node = parent.firstChild; node; node = node.nextSibling) if (node.nodeType === 1) out.push(node);
64
64
  return out;
65
65
  }
66
+ /**
67
+ * All *descendant* elements of `root` matching a namespace + local name, snapshotted into an
68
+ * array. Unlike {@link getElements} (direct children only) this searches the whole subtree, and
69
+ * unlike the live `HTMLCollection` that `getElementsByTagNameNS` returns, the result does not
70
+ * change as the caller mutates the tree — which is what makes it safe to iterate while replacing
71
+ * the very elements being iterated.
72
+ *
73
+ * Note the DOM's own exclusion: `root` itself is never a candidate, only its descendants.
74
+ */
75
+ function descendantsByTag(root, ns, local) {
76
+ const out = [];
77
+ for (const element of root.getElementsByTagNameNS(ns, local)) out.push(element);
78
+ return out;
79
+ }
66
80
  /** First direct child element matching a qname, or `null`. */
67
81
  function firstChild(parent, qname) {
68
82
  const { uri, local } = splitQName(qname);
@@ -216,6 +230,20 @@ function escapeXmlAttribute(value) {
216
230
  * A partname is an absolute, `/`-separated path inside the package, e.g.
217
231
  * `/ppt/slides/slide1.xml`. Zip entry paths omit the leading slash.
218
232
  */
233
+ /**
234
+ * Resolve the single relationship of `type` owned by `partName` to its target partname, or
235
+ * `null` when the part declares none. For the one-of-a-kind links in a deck's spine — a slide to
236
+ * its layout, a layout to its master, a master to its theme — where a second relationship of the
237
+ * same type would itself be malformed.
238
+ *
239
+ * The `OpcPackage` import is type-only, so this does not create a cycle with `package.ts` (which
240
+ * imports this module at run time).
241
+ */
242
+ function resolveSingleRel(opc, partName, type) {
243
+ const rels = opc.relationshipsFor(partName);
244
+ const rel = rels.byType(type)[0];
245
+ return rel ? rels.resolveTarget(rel.id) : null;
246
+ }
219
247
  function zipPathToPartName(zipPath) {
220
248
  return zipPath.startsWith("/") ? zipPath : `/${zipPath}`;
221
249
  }
@@ -272,16 +300,11 @@ function resolveRelativePartName(sourcePartName, target) {
272
300
  return `/${segments.join("/")}`;
273
301
  }
274
302
  //#endregion
275
- //#region src/read/oxml/fill.ts
303
+ //#region src/ooxml/sequence.ts
276
304
  /**
277
- * Shared solid-fill read/mutation helpers for DrawingML colour-bearing parents:
278
- * a run's `a:rPr`, a shape's `p:spPr` / `p:grpSpPr`, and a line's `a:ln`.
279
- *
280
- * A parent carries at most one `EG_FillProperties` choice, so setting a solid
281
- * fill first clears any competing choice, then inserts `a:solidFill` in document
282
- * order. These helpers never mark a part dirty — callers own that.
305
+ * `EG_FillProperties` — the mutually exclusive fill choices, in schema order.
306
+ * @see ECMA-376 Part 1 §20.1.8 (DrawingML fill group)
283
307
  */
284
- /** The mutually-exclusive fill choices (`EG_FillProperties`); a parent has at most one. */
285
308
  const FILL_CHOICES = [
286
309
  "a:noFill",
287
310
  "a:solidFill",
@@ -290,6 +313,143 @@ const FILL_CHOICES = [
290
313
  "a:pattFill",
291
314
  "a:grpFill"
292
315
  ];
316
+ /** `EG_EffectProperties` — the two mutually exclusive effect containers. */
317
+ const EFFECT_CHOICES = ["a:effectLst", "a:effectDag"];
318
+ /** `EG_Geometry` — the two mutually exclusive geometry choices. */
319
+ const GEOMETRY_CHOICES = ["a:custGeom", "a:prstGeom"];
320
+ /**
321
+ * Everything that legally follows `member` in `sequence`, flattened.
322
+ *
323
+ * `member` may name a single element or any member of a choice group; either way the result
324
+ * starts after the step it occupies, so a choice member never lists its own siblings. Throws on
325
+ * an unknown name rather than returning `[]` — an empty successor list silently *appends*, which
326
+ * is exactly the corruption these lists exist to prevent, so a typo must fail loudly and at
327
+ * module-load time rather than produce a subtly out-of-order part at run time.
328
+ */
329
+ function successorsOf(sequence, member) {
330
+ const index = sequence.findIndex((step) => typeof step === "string" ? step === member : step.includes(member));
331
+ if (index < 0) throw new Error(`successorsOf: ${member} is not part of the given sequence`);
332
+ return sequence.slice(index + 1).flat();
333
+ }
334
+ /**
335
+ * `CT_ShapeProperties` (`p:spPr`), in declaration order.
336
+ * @see ECMA-376 Part 1 §20.1.2.2.35
337
+ */
338
+ const SPPR_SEQUENCE = [
339
+ "a:xfrm",
340
+ GEOMETRY_CHOICES,
341
+ FILL_CHOICES,
342
+ "a:ln",
343
+ EFFECT_CHOICES,
344
+ "a:scene3d",
345
+ "a:sp3d",
346
+ "a:extLst"
347
+ ];
348
+ /**
349
+ * `CT_GroupShapeProperties` (`p:grpSpPr`), in declaration order. A group carries no `a:ln` and
350
+ * no `a:sp3d` — which is exactly why it needs its own successor lists rather than borrowing the
351
+ * shape ones.
352
+ * @see ECMA-376 Part 1 §20.1.2.2.19
353
+ */
354
+ const GRPSPPR_SEQUENCE = [
355
+ "a:xfrm",
356
+ FILL_CHOICES,
357
+ EFFECT_CHOICES,
358
+ "a:scene3d",
359
+ "a:extLst"
360
+ ];
361
+ /**
362
+ * `CT_LineProperties` (`a:ln`), in declaration order.
363
+ * @see ECMA-376 Part 1 §20.1.2.2.24
364
+ */
365
+ const LN_SEQUENCE = [
366
+ FILL_CHOICES,
367
+ ["a:prstDash", "a:custDash"],
368
+ [
369
+ "a:round",
370
+ "a:bevel",
371
+ "a:miter"
372
+ ],
373
+ "a:headEnd",
374
+ "a:tailEnd",
375
+ "a:extLst"
376
+ ];
377
+ /**
378
+ * `CT_TextCharacterProperties` (`a:rPr`/`a:defRPr`/`a:endParaRPr`), in declaration order.
379
+ * @see ECMA-376 Part 1 §21.1.2.3.9
380
+ */
381
+ const RPR_SEQUENCE = [
382
+ "a:ln",
383
+ FILL_CHOICES,
384
+ EFFECT_CHOICES,
385
+ "a:highlight",
386
+ ["a:uLnTx", "a:uLn"],
387
+ ["a:uFillTx", "a:uFill"],
388
+ "a:latin",
389
+ "a:ea",
390
+ "a:cs",
391
+ "a:sym",
392
+ "a:hlinkClick",
393
+ "a:hlinkMouseOver",
394
+ "a:rtl",
395
+ "a:extLst"
396
+ ];
397
+ /**
398
+ * `CT_TableCellProperties` (`a:tcPr`), in declaration order.
399
+ * @see ECMA-376 Part 1 §21.1.3.17
400
+ */
401
+ const TCPR_SEQUENCE = [
402
+ "a:lnL",
403
+ "a:lnR",
404
+ "a:lnT",
405
+ "a:lnB",
406
+ "a:lnTlToBr",
407
+ "a:lnBlToTr",
408
+ "a:cell3D",
409
+ ...FILL_CHOICES,
410
+ "a:headers",
411
+ "a:extLst"
412
+ ];
413
+ /**
414
+ * For each `a:tcPr` child, the children that must follow it — i.e. what a new one is inserted
415
+ * *before*. Every child gets an entry, so a caller can look one up by qname.
416
+ */
417
+ const TCPR_AFTER = Object.fromEntries(TCPR_SEQUENCE.map((qname, index) => [qname, TCPR_SEQUENCE.slice(index + 1)]));
418
+ /** Successors of `a:xfrm` inside `p:spPr`. */
419
+ const SPPR_AFTER_XFRM = successorsOf(SPPR_SEQUENCE, "a:xfrm");
420
+ /** Successors of a fill choice inside `p:spPr`. */
421
+ const SPPR_FILL_AFTER = successorsOf(SPPR_SEQUENCE, "a:solidFill");
422
+ /** Successors of `a:ln` inside `p:spPr`. */
423
+ const SPPR_LN_AFTER = successorsOf(SPPR_SEQUENCE, "a:ln");
424
+ /** Successors of an effect container inside `p:spPr`. */
425
+ const SPPR_EFFECT_AFTER = successorsOf(SPPR_SEQUENCE, "a:effectLst");
426
+ /** Successors of `a:scene3d` inside `p:spPr`. */
427
+ const SPPR_SCENE3D_AFTER = successorsOf(SPPR_SEQUENCE, "a:scene3d");
428
+ /** Successors of `a:sp3d` inside `p:spPr`. */
429
+ const SPPR_SP3D_AFTER = successorsOf(SPPR_SEQUENCE, "a:sp3d");
430
+ /** Successors of `a:xfrm` inside `p:grpSpPr`. */
431
+ const GRPSPPR_AFTER_XFRM = successorsOf(GRPSPPR_SEQUENCE, "a:xfrm");
432
+ /** Successors of a fill choice inside `p:grpSpPr`. */
433
+ const GRPSPPR_FILL_AFTER = successorsOf(GRPSPPR_SEQUENCE, "a:solidFill");
434
+ /**
435
+ * Successors of `p:spPr` within `p:sp` (and before `p:style` within `p:pic` / `p:cxnSp`).
436
+ * `p:blipFill` and the `p:nv*Pr` wrapper precede `p:spPr` and so are deliberately absent.
437
+ */
438
+ const SHAPE_AFTER_SPPR = ["p:style", "p:txBody"];
439
+ /** Successors of a fill choice inside `a:ln`. */
440
+ const LN_FILL_AFTER = successorsOf(LN_SEQUENCE, "a:solidFill");
441
+ /** Successors of a fill choice inside `a:rPr` (and `a:defRPr`/`a:endParaRPr`). */
442
+ const RPR_FILL_AFTER = successorsOf(RPR_SEQUENCE, "a:solidFill");
443
+ //#endregion
444
+ //#region src/read/oxml/fill.ts
445
+ /**
446
+ * Shared solid-fill read/mutation helpers for DrawingML colour-bearing parents:
447
+ * a run's `a:rPr`, a shape's `p:spPr` / `p:grpSpPr`, and a line's `a:ln`.
448
+ *
449
+ * A parent carries at most one `EG_FillProperties` choice, so setting a solid
450
+ * fill first clears any competing choice, then inserts `a:solidFill` in document
451
+ * order. These helpers never mark a part dirty — callers own that.
452
+ */
293
453
  /** Normalize a 6-hex RGB string (optional leading `#`) to upper-case, or throw. */
294
454
  function normalizeHex(value) {
295
455
  const hex = value.startsWith("#") ? value.slice(1) : value;
@@ -318,41 +478,27 @@ function setSolidFill(parent, afterOrder, color) {
318
478
  //#endregion
319
479
  //#region src/read/oxml/theme.ts
320
480
  /**
321
- * Theme resolution for the {@link Presentation.importSlide} `theme: 'preserve'`
322
- * mode: bake the values a slide's *source* theme would have produced into the
323
- * slide XML, so the slide no longer depends on which theme it resolves against.
481
+ * Theme colour resolution: turning a DrawingML colour reference or a style-matrix reference into
482
+ * something literal.
324
483
  *
325
- * Two kinds of theme dependency are flattened (see IMPORT_SLIDE_THEME_PRESERVE):
484
+ * Two maps do the colour work. A scheme token (`accent1`, `bg1`, …) routes through the master's
485
+ * `clrMap` to a `clrScheme` slot, whose literal RGB is the answer; child colour transforms
486
+ * (`lumMod`/`shade`/`alpha`/…) are carried through untouched rather than computed, so a caller
487
+ * that re-emits them renders identically. A style-matrix reference (`a:fillRef`/`a:lnRef`) instead
488
+ * indexes the theme's `fmtScheme`, and resolves to a deep-cloned entry with its `phClr`
489
+ * placeholder substituted.
326
490
  *
327
- * 1. **`a:schemeClr` tokens** — a scheme token (`accent1`, `bg1`, …) routes
328
- * through the master's `clrMap` to a `clrScheme` slot, whose literal RGB we
329
- * emit as `a:srgbClr`. Child colour transforms (`lumMod`/`shade`/`alpha`/…)
330
- * are carried through untouched, so tints/shades render identically — we swap
331
- * only the *base* colour reference, never compute the transform math.
332
- * 2. **Style-matrix refs** — a shape's `p:style` (`lnRef`/`fillRef`/`effectRef`)
333
- * indexes the theme's `fmtScheme`; we resolve the indexed entry into an
334
- * explicit `spPr` fill/line/effect (substituting its `phClr` placeholder with
335
- * the ref's own colour) and neutralize the ref so it cannot re-resolve. The
336
- * `fontRef` is left intact so its font (and resolved colour) can re-bind to
337
- * the destination theme — the deliberate "normalize fonts on attach" bonus.
338
- * 3. **Placeholder-inherited run colour** — a run whose `a:rPr` carries no own
339
- * fill takes its colour from the source placeholder → layout → master text
340
- * style chain (`p:txStyles` / placeholder `a:lstStyle`). Rebinding to the
341
- * destination master would replace that chain and flip the colour, so we
342
- * resolve each such run's effective colour from the *source* styles and write
343
- * it explicitly onto the run.
344
- * 4. **Placeholder-inherited geometry & run size** — a placeholder shape with no
345
- * own `a:xfrm` takes its position/size from the matching source layout/master
346
- * placeholder, and a run with no own `sz`/`b`/`i` takes them from the same text
347
- * style chain. Rebinding to the destination master replaces both inheritances,
348
- * so a title clips off-canvas and type comes out at the wrong size. We bake the
349
- * effective `a:xfrm` onto the shape and the effective `sz`/`b`/`i` onto each run.
350
- * Typeface (`a:latin`) is deliberately *not* baked — it re-binds to the
351
- * destination theme along with `fontRef`.
491
+ * Everything here is **pure**: it reads a DOM and builds detached elements, and mutates nothing
492
+ * the caller passes in. Two very different consumers depend on that:
352
493
  *
353
- * All functions operate purely on DOM elements; the caller gathers the source
354
- * `clrMap` / `clrScheme` / `fmtScheme` parts (and the source layout/master roots
355
- * for the placeholder inheritance passes) and owns marking the slide dirty.
494
+ * - the read model's colour getters, which resolve a colour to report it;
495
+ * - the import-time flatten passes (`read/api/ops/flatten.ts`), which resolve the same colour in
496
+ * order to *bake* it into a slide.
497
+ *
498
+ * They must agree, so they share this. The passes that write results back into a part live with
499
+ * the other import operations, not here — this module deliberately has no mutating exports.
500
+ * Placeholder-inheritance resolution (what a placeholder gets from its layout/master chain) is
501
+ * its own concern and lives in `placeholder-inherit.ts`.
356
502
  */
357
503
  /** The 12 `a:clrScheme` slot names, in schema order. */
358
504
  const SCHEME_SLOTS = [
@@ -376,39 +522,6 @@ const DIRECT_SLOT_TOKENS = /* @__PURE__ */ new Set([
376
522
  "dk2",
377
523
  "lt2"
378
524
  ]);
379
- /** Schema successors for ordered insertion into `p:spPr` (CT_ShapeProperties). */
380
- const SPPR_XFRM_AFTER = [
381
- "a:custGeom",
382
- "a:prstGeom",
383
- ...FILL_CHOICES,
384
- "a:ln",
385
- "a:effectLst",
386
- "a:effectDag",
387
- "a:scene3d",
388
- "a:sp3d",
389
- "a:extLst"
390
- ];
391
- const SPPR_FILL_AFTER$1 = [
392
- "a:ln",
393
- "a:effectLst",
394
- "a:effectDag",
395
- "a:scene3d",
396
- "a:sp3d",
397
- "a:extLst"
398
- ];
399
- const SPPR_LN_AFTER$1 = [
400
- "a:effectLst",
401
- "a:effectDag",
402
- "a:scene3d",
403
- "a:sp3d",
404
- "a:extLst"
405
- ];
406
- const SPPR_EFFECT_AFTER = [
407
- "a:scene3d",
408
- "a:sp3d",
409
- "a:extLst"
410
- ];
411
- const SHAPE_AFTER_SPPR$1 = ["p:style", "p:txBody"];
412
525
  /** Parse an `a:clrScheme` into slot → 6-hex RGB, reading `srgbClr`/`sysClr`. */
413
526
  function parseClrScheme(clrScheme) {
414
527
  const out = /* @__PURE__ */ new Map();
@@ -452,12 +565,6 @@ function resolveSchemeToken(token, ctx) {
452
565
  const slot = DIRECT_SLOT_TOKENS.has(token) ? token : ctx.clrMap.get(token);
453
566
  return slot ? ctx.clrScheme.get(slot) ?? null : null;
454
567
  }
455
- /** Direct child *elements* of `parent`, in order. */
456
- function childElements$1(parent) {
457
- const out = [];
458
- for (let node = parent.firstChild; node; node = node.nextSibling) if (node.nodeType === 1) out.push(node);
459
- return out;
460
- }
461
568
  /** Whether `el` is a DrawingML element with the given local name. */
462
569
  function isA(el, local) {
463
570
  return !!el && el.namespaceURI === OOXML_NS.a && el.localName === local;
@@ -476,7 +583,7 @@ function colorElementHex(color) {
476
583
  */
477
584
  function resolveColor(color, ctx) {
478
585
  if (!color) return null;
479
- const transforms = childElements$1(color);
586
+ const transforms = childElements(color);
480
587
  if (isA(color, "srgbClr")) {
481
588
  const hex = attr(color, "val");
482
589
  return hex ? {
@@ -502,239 +609,128 @@ function resolveColor(color, ctx) {
502
609
  return null;
503
610
  }
504
611
  /**
505
- * Flatten one slide's theme dependencies in place:
506
- *
507
- * 1. carry the background the slide inherited from its source layout/master onto
508
- * the slide (so it survives rebinding to a different master);
509
- * 2. materialize `p:bgRef` and style-matrix refs into explicit fills/lines/effects;
510
- * 3. bake each placeholder's effective geometry (inherited `a:xfrm`) onto the
511
- * shape so a rebind cannot move or resize it;
512
- * 4. bake each placeholder run's effective colour and size/weight (inherited from
513
- * the source layout/master text styles) explicitly onto the run;
514
- * 5. rewrite every remaining `a:schemeClr` to its literal `a:srgbClr`.
515
- *
516
- * Steps run in this order so the inherited/materialized backgrounds are present
517
- * before the final scheme-colour sweep resolves the colours they carry. The
518
- * placeholder geometry/colour/size passes have no data dependency on the others.
519
- * The caller marks the part dirty.
612
+ * Resolve a `+mj-*`/`+mn-*` major/minor font token against a theme `a:fontScheme`
613
+ * to a literal typeface name. A non-token `typeface` (an already-literal face) is
614
+ * returned verbatim. `null` when `typeface` is absent, or when a token cannot be
615
+ * resolved (no `fontScheme`, or the indexed major/minor script slot is empty).
616
+ * The script suffix selects the scheme child: `lt`→`a:latin`, `ea`→`a:ea`,
617
+ * `cs`→`a:cs`.
520
618
  */
521
- function flattenSlide(slideRoot, ctx) {
522
- applyInheritedBackground(slideRoot, ctx);
523
- materializeBackground(slideRoot, ctx);
524
- materializeStyleRefs(slideRoot, ctx);
525
- resolvePlaceholderGeometry(slideRoot, ctx);
526
- resolvePlaceholderRunColors(slideRoot, ctx);
527
- resolvePlaceholderRunSizes(slideRoot, ctx);
528
- resolveSchemeColors(slideRoot, ctx);
619
+ function resolveThemeFont(typeface, fontScheme) {
620
+ if (!typeface) return null;
621
+ const match = /^\+(mj|mn)-(lt|ea|cs)$/.exec(typeface);
622
+ if (!match) return typeface;
623
+ if (!fontScheme) return null;
624
+ const font = firstChild(fontScheme, match[1] === "mj" ? "a:majorFont" : "a:minorFont");
625
+ const childName = match[2] === "lt" ? "a:latin" : match[2] === "ea" ? "a:ea" : "a:cs";
626
+ const child = font && firstChild(font, childName);
627
+ return child && attr(child, "typeface") || null;
529
628
  }
530
- /**
531
- * Flatten a single lifted shape's theme dependencies in place — the shape-scoped
532
- * subset of {@link flattenSlide} used by `Presentation.importShape` `preserve`
533
- * mode. It runs every pass that resolves a *shape's* theme references against the
534
- * source theme (style-matrix refs, placeholder-inherited geometry/colour/size/
535
- * anchor/list-style, scheme colours) but deliberately **omits** the two
536
- * slide-scoped background passes (`applyInheritedBackground`/`materializeBackground`):
537
- * a background belongs to a slide, not to a shape being composed onto a foreign host.
538
- *
539
- * Unlike {@link flattenSlide}, this also **demotes** a lifted placeholder to a
540
- * plain shape ({@link demotePlaceholders}) once everything it inherited is baked.
541
- * A placeholder makes sense on its own slide, where it resolves against that deck's
542
- * master/layout; lifted onto a foreign host its surviving `p:ph` would re-resolve
543
- * against the *host* placeholder of the same type/idx (wrong inheritance, or a
544
- * fallback when absent) and could collide with the host's own placeholder. The
545
- * extra bakes here (anchor + list style, on top of the geometry/colour/size the
546
- * shared passes cover) make the shape self-contained so demotion loses nothing
547
- * visible. This is why the demotion is scoped to `flattenShape`: `flattenSlide`
548
- * keeps placeholders as placeholders by design.
549
- *
550
- * `shapeRoot` must be an element whose *descendants* include the lifted shape
551
- * (the passes match via `getElementsByTagNameNS`, which excludes the root element
552
- * itself) — the caller wraps the imported `p:sp`/`p:pic`/`p:graphicFrame`/`p:grpSp`
553
- * in a throwaway container before calling. The caller marks the part dirty.
554
- */
555
- function flattenShape(shapeRoot, ctx) {
556
- materializeStyleRefs(shapeRoot, ctx);
557
- resolvePlaceholderGeometry(shapeRoot, ctx);
558
- resolvePlaceholderRunColors(shapeRoot, ctx);
559
- resolvePlaceholderRunSizes(shapeRoot, ctx);
560
- resolvePlaceholderBodyPr(shapeRoot, ctx);
561
- resolvePlaceholderListStyle(shapeRoot, ctx);
562
- resolveSchemeColors(shapeRoot, ctx);
563
- demotePlaceholders(shapeRoot);
629
+ /** Replace every `phClr` under `el` with the ref colour (ref transforms first, then the `phClr`'s own). */
630
+ function substitutePhClr(el, ref) {
631
+ const doc = ownerDocumentOf(el);
632
+ for (const phClr of descendantsByTag(el, OOXML_NS.a, "schemeClr")) {
633
+ if (attr(phClr, "val") !== "phClr") continue;
634
+ const srgb = createElement(doc, "a:srgbClr");
635
+ setAttr(srgb, "val", ref.hex);
636
+ for (const t of ref.transforms) srgb.appendChild(t.cloneNode(true));
637
+ while (phClr.firstChild) srgb.appendChild(phClr.firstChild);
638
+ replaceInParent(phClr, srgb);
639
+ }
564
640
  }
565
- /**
566
- * Restyle a rebound slide in place (the `theme: 'restyle'` mode): the exact
567
- * inverse of {@link flattenSlide}. It bakes *nothing* — leaving every
568
- * `a:schemeClr`/style-matrix ref and `p:bg` `bgRef` symbolic is the whole point,
569
- * so they re-resolve against the *destination* master's `clrMap` + theme once the
570
- * slide is rebound. The single mutation is dropping the slide's own colour-map
571
- * override (`p:clrMapOvr/a:overrideClrMapping`): a source override would keep
572
- * mapping the slide's scheme-token names the source way and defeat the re-brand,
573
- * so it must yield to the destination master's `p:clrMap`.
574
- *
575
- * Caveat carried from the plan: only *symbolic* colours re-brand. Anything the
576
- * source authored as a literal `a:srgbClr` has no theme reference to re-resolve
577
- * and stays exactly that colour. The caller marks the part dirty.
578
- */
579
- function restyleSlide(slideRoot) {
580
- removeChildrenByQName(slideRoot, ["p:clrMapOvr"]);
641
+ /** The `idx`-th entry (1-based) of a `fmtScheme` style list, deep-cloned, or `null`. */
642
+ function fmtEntry(ctx, listName, idx) {
643
+ const list = ctx.fmtScheme && firstChild(ctx.fmtScheme, listName);
644
+ if (!list || idx < 1) return null;
645
+ const entry = childElements(list)[idx - 1];
646
+ return entry ? entry.cloneNode(true) : null;
581
647
  }
582
648
  /**
583
- * The optional inverse of {@link resolveSchemeColors}, for `restyle`'s
584
- * force-remap-literals mode (the {@link Presentation.importSlide} `remapLiterals`
585
- * option). Every literal `a:srgbClr` whose value equals a *source* `clrScheme`
586
- * slot is rewritten back to a symbolic `a:schemeClr` so it re-resolves against the
587
- * destination theme; a literal matching no slot (the common case) is left exactly
588
- * as authored. Transform children (`lumMod`/`shade`/`alpha`/…) are carried across.
589
- *
590
- * Plain `restyle` cannot re-brand a literal — it carries no theme reference. This
591
- * re-introduces one by matching the literal's RGB against the slots the *source*
592
- * theme defined, emitting the scheme token that routes through the source `clrMap`
593
- * (so a literal equal to the source `accent1` slot becomes `schemeClr accent1`,
594
- * which the destination `clrMap`/theme then re-resolve). Slots are matched in
595
- * `clrScheme` order, so the first slot defining a given RGB wins when several share
596
- * it. Opt-in because it deliberately reinterprets authored literals as theme
597
- * colours — visual QA territory, the same caveat as the rest of `restyle`.
649
+ * Build the explicit fill element a `p:style` `a:fillRef` resolves to — the indexed
650
+ * `fmtScheme` `fillStyleLst`/`bgFillStyleLst` entry (deep-cloned) with its `phClr`
651
+ * replaced by the ref's resolved colour. Pure: mutates neither the ref nor the
652
+ * theme. `null` when the ref is absent, `idx` is 0/unset, or the entry or its
653
+ * colour cannot be resolved. Shared by the flatten path and the read-model
654
+ * `resolveStyleFillColor` getter so both see the same fill.
598
655
  */
599
- function remapLiteralColors(slideRoot, ctx) {
600
- const slotByHex = reverseClrScheme(ctx.clrScheme);
601
- if (slotByHex.size === 0) return;
602
- const tokenBySlot = reverseClrMap(ctx.clrMap);
603
- const doc = ownerDocumentOf(slideRoot);
604
- for (const srgb of elementsByTag(slideRoot, OOXML_NS.a, "srgbClr")) {
605
- const hex = attr(srgb, "val");
606
- const slot = hex ? slotByHex.get(hex.toUpperCase()) : void 0;
607
- if (!slot) continue;
608
- const scheme = createElement(doc, "a:schemeClr");
609
- setAttr(scheme, "val", tokenBySlot.get(slot) ?? slot);
610
- while (srgb.firstChild) scheme.appendChild(srgb.firstChild);
611
- replaceInParent(srgb, scheme);
612
- }
613
- }
614
- /** Reverse a slot → RGB `clrScheme` into RGB → slot, in `SCHEME_SLOTS` order (first slot wins on a shared RGB). */
615
- function reverseClrScheme(clrScheme) {
616
- const out = /* @__PURE__ */ new Map();
617
- for (const slot of SCHEME_SLOTS) {
618
- const hex = clrScheme.get(slot);
619
- if (hex && !out.has(hex.toUpperCase())) out.set(hex.toUpperCase(), slot);
620
- }
621
- return out;
622
- }
623
- /** Reverse a token → slot `clrMap` into slot → token (first token wins on a shared slot). */
624
- function reverseClrMap(clrMap) {
625
- const out = /* @__PURE__ */ new Map();
626
- for (const [token, slot] of clrMap) if (!out.has(slot)) out.set(slot, token);
627
- return out;
656
+ function styleRefFill(fillRef, ctx) {
657
+ if (!fillRef) return null;
658
+ const idx = intAttr(fillRef, "idx");
659
+ if (idx === null || idx <= 0) return null;
660
+ const fill = idx >= 1e3 ? fmtEntry(ctx, "a:bgFillStyleLst", idx - 1e3) : fmtEntry(ctx, "a:fillStyleLst", idx);
661
+ const ref = resolveColor(firstChildElement(fillRef), ctx);
662
+ if (!fill || !ref) return null;
663
+ substitutePhClr(fill, ref);
664
+ return fill;
628
665
  }
629
666
  /**
630
- * If the slide has no own `p:bg`, insert (a copy of) the background it inherited
631
- * from its source layout/master as an explicit `p:cSld/p:bg`. The clone is left
632
- * unresolved here; the later passes flatten its `bgRef`/`schemeClr` in place.
667
+ * Build the explicit `a:ln` element a `p:style` `a:lnRef` resolves to — the indexed
668
+ * `fmtScheme` `lnStyleLst` entry (deep-cloned) with its `phClr` replaced by the
669
+ * ref's resolved colour. Pure; the line/read counterpart of {@link styleRefFill}.
633
670
  */
634
- function applyInheritedBackground(slideRoot, ctx) {
635
- const inherited = ctx.inheritedBackground;
636
- if (!inherited) return;
637
- const cSld = firstChild(slideRoot, "p:cSld");
638
- if (!cSld || firstChild(cSld, "p:bg")) return;
639
- insertInOrder(cSld, ownerDocumentOf(slideRoot).importNode(inherited, true), [
640
- "p:spTree",
641
- "p:custDataLst",
642
- "p:controls",
643
- "p:extLst"
644
- ]);
671
+ function styleRefLine(lnRef, ctx) {
672
+ if (!lnRef) return null;
673
+ const idx = intAttr(lnRef, "idx");
674
+ if (idx === null || idx <= 0) return null;
675
+ const ln = fmtEntry(ctx, "a:lnStyleLst", idx);
676
+ const ref = resolveColor(firstChildElement(lnRef), ctx);
677
+ if (!ln || !ref) return null;
678
+ substitutePhClr(ln, ref);
679
+ return ln;
645
680
  }
681
+ /** Read an integer attribute; `null`/empty/non-finite → `null`. */
682
+ function intAttr(el, name) {
683
+ const value = attr(el, name);
684
+ if (value === null || value === "") return null;
685
+ const n = Number(value);
686
+ return Number.isFinite(n) ? n : null;
687
+ }
688
+ //#endregion
689
+ //#region src/read/oxml/placeholder-inherit.ts
646
690
  /**
647
- * Resolve every `p:bgRef` (theme-indexed background) under the slide into an
648
- * explicit `p:bgPr` fill, so the background no longer depends on the destination
649
- * theme's `fmtScheme`. A `bgPr` background is left for the scheme-colour sweep.
691
+ * Resolving what a placeholder shape *inherits* from its source layout/master chain.
692
+ *
693
+ * A placeholder that sets none of its own colour, size, geometry, anchor or list style takes
694
+ * each from a chain: the layout placeholder's `a:lstStyle`, then the master placeholder's, then
695
+ * the master's `p:txStyles` category style (and, on a notes context, the notesMaster
696
+ * `p:notesStyle`). Every function here answers "what would this placeholder get?" — none of them
697
+ * writes anything.
698
+ *
699
+ * **Two callers, one answer.** The read model's getters (`Run.resolvedColor`,
700
+ * `TextFrame.resolvedAnchor`, the font getters) and the import-time flatten passes
701
+ * (`read/api/ops/flatten.ts`, which bakes the inherited values onto a slide so they survive a
702
+ * rebind to a different master) both ask the same questions. They must not answer them
703
+ * differently: if the getter and the bake disagreed, a caller would see one colour before export
704
+ * and another in the file. Sharing this module is what makes that structurally impossible.
705
+ *
706
+ * This module is pure — mutation lives in the flatten pass, which is also why it sits in
707
+ * `read/oxml/` (substrate) rather than `read/api/ops/`.
650
708
  */
651
- function materializeBackground(slideRoot, ctx) {
652
- const doc = ownerDocumentOf(slideRoot);
653
- for (const bg of elementsByTag(slideRoot, OOXML_NS.p, "bg")) {
654
- const bgRef = firstChild(bg, "p:bgRef");
655
- if (!bgRef) continue;
656
- const fill = styleRefFill(bgRef, ctx);
657
- const bgPr = createElement(doc, "p:bgPr");
658
- bgPr.appendChild(fill ?? createElement(doc, "a:noFill"));
659
- bg.replaceChild(bgPr, bgRef);
660
- }
661
- }
662
- /** Rewrite every `a:schemeClr` under `root` to a literal `a:srgbClr` when resolvable. */
663
- function resolveSchemeColors(root, ctx) {
664
- const doc = ownerDocumentOf(root);
665
- for (const schemeClr of elementsByTag(root, OOXML_NS.a, "schemeClr")) {
666
- const token = attr(schemeClr, "val");
667
- const hex = token ? resolveSchemeToken(token, ctx) : null;
668
- if (!hex) continue;
669
- const srgb = createElement(doc, "a:srgbClr");
670
- setAttr(srgb, "val", hex);
671
- while (schemeClr.firstChild) srgb.appendChild(schemeClr.firstChild);
672
- replaceInParent(schemeClr, srgb);
673
- }
674
- }
675
- /** Schema successors of `a:solidFill` inside `a:rPr` (CT_TextCharacterProperties sequence). */
676
- const RPR_FILL_AFTER = [
677
- "a:effectLst",
678
- "a:effectDag",
679
- "a:highlight",
680
- "a:uLnTx",
681
- "a:uLn",
682
- "a:uFillTx",
683
- "a:uFill",
684
- "a:latin",
685
- "a:ea",
686
- "a:cs",
687
- "a:sym",
688
- "a:hlinkClick",
689
- "a:hlinkMouseOver",
690
- "a:rtl",
691
- "a:extLst"
692
- ];
693
709
  /** The master `p:txStyles` style element name for a placeholder category. */
694
710
  const TX_STYLE_NAME = {
695
711
  title: "p:titleStyle",
696
712
  body: "p:bodyStyle",
697
713
  other: "p:otherStyle"
698
714
  };
699
- /**
700
- * Bake placeholder-inherited run colours onto the slide (gap 1). For each
701
- * placeholder run that defines no colour of its own (nor at paragraph/text-body
702
- * level on the *slide*), resolve the colour it would inherit from the source
703
- * `slideLayout`/`slideMaster` text styles and write it explicitly onto the run's
704
- * `a:rPr`. After this the run's colour cannot change when the slide is rebound to
705
- * the destination master. `a:fld` runs (dates, slide numbers) are treated like
706
- * `a:r`. Only colour is resolved; other inheritable run properties are left to
707
- * re-bind to the destination styles.
708
- */
709
- function resolvePlaceholderRunColors(slideRoot, ctx) {
710
- if (!ctx.layoutRoot && !ctx.masterRoot) return;
711
- for (const sp of elementsByTag(slideRoot, OOXML_NS.p, "sp")) {
712
- const ph = placeholderOf(sp);
713
- if (!ph) continue;
714
- const txBody = firstChild(sp, "p:txBody");
715
- if (!txBody) continue;
716
- const type = attr(ph, "type");
717
- const idx = attr(ph, "idx") ?? "0";
718
- const slideLst = firstChild(txBody, "a:lstStyle");
719
- const byLevel = /* @__PURE__ */ new Map();
720
- for (const p of getElements(txBody, "a:p")) {
721
- const pPr = firstChild(p, "a:pPr");
722
- const level = (pPr && intValue(attr(pPr, "lvl"))) ?? 0;
723
- const runs = [...getElements(p, "a:r"), ...getElements(p, "a:fld")];
724
- if (runs.length === 0) continue;
725
- let color = byLevel.get(level);
726
- if (color === void 0) {
727
- color = placeholderInheritedColor(type, idx, level, ctx);
728
- byLevel.set(level, color);
729
- }
730
- if (!color) continue;
731
- for (const run of runs) {
732
- if (slideDefinesColor(run, pPr, slideLst, level)) continue;
733
- writeRunColor(run, color);
734
- }
735
- }
736
- }
737
- }
715
+ /** The ordered children of a `CT_TextListStyle` (`a:lstStyle` / a `p:txStyles` style). */
716
+ const LST_STYLE_LEVELS = [
717
+ "a:defPPr",
718
+ "a:lvl1pPr",
719
+ "a:lvl2pPr",
720
+ "a:lvl3pPr",
721
+ "a:lvl4pPr",
722
+ "a:lvl5pPr",
723
+ "a:lvl6pPr",
724
+ "a:lvl7pPr",
725
+ "a:lvl8pPr",
726
+ "a:lvl9pPr"
727
+ ];
728
+ /** Inheritable run properties baked under `preserve`: size and weight/slant (not typeface). */
729
+ const RUN_PROP_NAMES = [
730
+ "sz",
731
+ "b",
732
+ "i"
733
+ ];
738
734
  /** The `p:ph` element of a shape (`p:sp/p:nvSpPr/p:nvPr/p:ph`), or `null`. */
739
735
  function placeholderOf(sp) {
740
736
  const nvSpPr = firstChild(sp, "p:nvSpPr");
@@ -784,7 +780,7 @@ const SINGLETON_PH = /* @__PURE__ */ new Set([
784
780
  */
785
781
  function findPlaceholder(root, slideType, slideIdx) {
786
782
  if (slideType && SINGLETON_PH.has(slideType)) {
787
- for (const sp of elementsByTag(root, OOXML_NS.p, "sp")) {
783
+ for (const sp of descendantsByTag(root, OOXML_NS.p, "sp")) {
788
784
  const ph = placeholderOf(sp);
789
785
  if (ph && attr(ph, "type") === slideType) return sp;
790
786
  }
@@ -793,7 +789,7 @@ function findPlaceholder(root, slideType, slideIdx) {
793
789
  const cat = phCategory(slideType);
794
790
  let idxMatch = null;
795
791
  let catMatch = null;
796
- for (const sp of elementsByTag(root, OOXML_NS.p, "sp")) {
792
+ for (const sp of descendantsByTag(root, OOXML_NS.p, "sp")) {
797
793
  const ph = placeholderOf(sp);
798
794
  if (!ph) continue;
799
795
  const type = attr(ph, "type");
@@ -827,337 +823,80 @@ function lstStyleLevelFill(listStyle, level) {
827
823
  return defRPr ? firstChild(defRPr, "a:solidFill") : null;
828
824
  }
829
825
  /**
830
- * The colour *element* a placeholder run inherits from the source style chain: the
831
- * layout placeholder's `a:lstStyle`, then the master placeholder's, then the
832
- * master `p:txStyles` category style. Returns the first tier's colour element that
833
- * resolves against `ctx` (the `a:srgbClr`/`a:schemeClr`/… inside its
834
- * `a:solidFill`), or `null` when nothing in the chain defines a resolvable colour.
835
- * The read-model `Run.resolvedColor` getter feeds this element to
836
- * `resolveColorElement` for the `effectiveHex`; the flatten path resolves it
837
- * directly via {@link placeholderInheritedColor}.
838
- */
839
- function placeholderInheritedFill(type, idx, level, ctx) {
840
- const tiers = [];
841
- if (ctx.layoutRoot) {
842
- const layoutPh = findPlaceholder(ctx.layoutRoot, type, idx);
843
- tiers.push(layoutPh && lstStyleLevelFill(placeholderLstStyle(layoutPh), level));
844
- }
845
- if (ctx.masterRoot) {
846
- const masterPh = findPlaceholder(ctx.masterRoot, type, idx);
847
- tiers.push(masterPh && lstStyleLevelFill(placeholderLstStyle(masterPh), level));
848
- const txStyles = firstChild(ctx.masterRoot, "p:txStyles");
849
- const styleEl = txStyles && firstChild(txStyles, TX_STYLE_NAME[phCategory(type)]);
850
- tiers.push(styleEl && lstStyleLevelFill(styleEl, level));
851
- }
852
- if (ctx.notesStyle) tiers.push(lstStyleLevelFill(ctx.notesStyle, level));
853
- for (const fill of tiers) {
854
- const colorEl = fill && firstChildElement(fill);
855
- if (colorEl && resolveColor(colorEl, ctx)) return colorEl;
856
- }
857
- return null;
858
- }
859
- /**
860
- * The level `a:defRPr` elements a placeholder inherits from the source style
861
- * chain, in resolution order: the layout placeholder's `a:lstStyle`, then the
862
- * master placeholder's, then the master `p:txStyles` category style — and, on a
863
- * notes context only, the notesMaster `p:notesStyle` (see `ctx.notesStyle`). Tiers
864
- * with no `a:defRPr` for `level` are dropped. The shared root for inherited run
865
- * *size* and *typeface* resolution (the size/face sibling of
866
- * {@link placeholderInheritedFill}), read directly by the flatten path and the
867
- * read-model font getters.
826
+ * The source style tiers a placeholder resolves against, most specific first: the layout
827
+ * placeholder's `a:lstStyle`, the master placeholder's, then the master `p:txStyles` category
828
+ * style — and, on a notes context only, the notesMaster `p:notesStyle`.
829
+ *
830
+ * The one place the chain is spelled out. `placeholderInherited*` differ only in what they pull
831
+ * from each tier, so they all walk this.
868
832
  */
869
- function placeholderInheritedDefRPrs(type, idx, level, ctx) {
833
+ function styleTiers(type, idx, ctx) {
870
834
  const tiers = [];
871
835
  if (ctx.layoutRoot) {
872
836
  const layoutPh = findPlaceholder(ctx.layoutRoot, type, idx);
873
- tiers.push(layoutPh && lstStyleLevelDefRPr(placeholderLstStyle(layoutPh), level));
837
+ tiers.push(layoutPh && placeholderLstStyle(layoutPh));
874
838
  }
875
839
  if (ctx.masterRoot) {
876
840
  const masterPh = findPlaceholder(ctx.masterRoot, type, idx);
877
- tiers.push(masterPh && lstStyleLevelDefRPr(placeholderLstStyle(masterPh), level));
841
+ tiers.push(masterPh && placeholderLstStyle(masterPh));
878
842
  const txStyles = firstChild(ctx.masterRoot, "p:txStyles");
879
- const styleEl = txStyles && firstChild(txStyles, TX_STYLE_NAME[phCategory(type)]);
880
- tiers.push(styleEl && lstStyleLevelDefRPr(styleEl, level));
843
+ tiers.push(txStyles && firstChild(txStyles, TX_STYLE_NAME[phCategory(type)]));
881
844
  }
882
- if (ctx.notesStyle) tiers.push(lstStyleLevelDefRPr(ctx.notesStyle, level));
845
+ if (ctx.notesStyle) tiers.push(ctx.notesStyle);
883
846
  return tiers.filter((t) => t !== null);
884
847
  }
885
848
  /**
886
- * Resolve a `+mj-*`/`+mn-*` major/minor font token against a theme `a:fontScheme`
887
- * to a literal typeface name. A non-token `typeface` (an already-literal face) is
888
- * returned verbatim. `null` when `typeface` is absent, or when a token cannot be
889
- * resolved (no `fontScheme`, or the indexed major/minor script slot is empty).
890
- * The script suffix selects the scheme child: `lt`→`a:latin`, `ea`→`a:ea`,
891
- * `cs`→`a:cs`.
892
- */
893
- function resolveThemeFont(typeface, fontScheme) {
894
- if (!typeface) return null;
895
- const match = /^\+(mj|mn)-(lt|ea|cs)$/.exec(typeface);
896
- if (!match) return typeface;
897
- if (!fontScheme) return null;
898
- const font = firstChild(fontScheme, match[1] === "mj" ? "a:majorFont" : "a:minorFont");
899
- const childName = match[2] === "lt" ? "a:latin" : match[2] === "ea" ? "a:ea" : "a:cs";
900
- const child = font && firstChild(font, childName);
901
- return child && attr(child, "typeface") || null;
902
- }
903
- /**
904
- * The colour a placeholder run inherits from the source style chain, resolved to a
905
- * literal `{ hex, transforms }`. Thin wrapper over {@link placeholderInheritedFill}
906
- * for the flatten path, which re-emits the transforms verbatim. Returns `null`
907
- * when nothing in the chain defines a resolvable colour (the run then re-binds to
908
- * the destination).
909
- */
910
- function placeholderInheritedColor(type, idx, level, ctx) {
911
- const colorEl = placeholderInheritedFill(type, idx, level, ctx);
912
- return colorEl ? resolveColor(colorEl, ctx) : null;
913
- }
914
- /** Whether the *slide itself* already fixes this run's colour (so a rebind cannot change it). */
915
- function slideDefinesColor(run, pPr, slideLst, level) {
916
- const rPr = firstChild(run, "a:rPr");
917
- if (rPr && FILL_CHOICES.some((q) => firstChild(rPr, q))) return true;
918
- const defRPr = pPr && firstChild(pPr, "a:defRPr");
919
- if (defRPr && firstChild(defRPr, "a:solidFill")) return true;
920
- return !!lstStyleLevelFill(slideLst, level);
921
- }
922
- /** Write a resolved colour as an explicit `a:solidFill` (with carried transforms) onto a run's `a:rPr`. */
923
- function writeRunColor(run, color) {
924
- const doc = ownerDocumentOf(run);
925
- const rPr = getOrAddChild(run, "a:rPr", ["a:t"]);
926
- const fill = createElement(doc, "a:solidFill");
927
- const srgb = createElement(doc, "a:srgbClr");
928
- setAttr(srgb, "val", color.hex);
929
- for (const t of color.transforms) srgb.appendChild(t.cloneNode(true));
930
- fill.appendChild(srgb);
931
- insertInOrder(rPr, fill, RPR_FILL_AFTER);
932
- }
933
- /**
934
- * Bake placeholder-inherited geometry onto the slide (gap 3). A placeholder shape
935
- * that carries no own `p:spPr/a:xfrm` takes its position/size from the matching
936
- * source `slideLayout` placeholder, else the `slideMaster` placeholder. Rebinding
937
- * to the destination master replaces that inheritance, so the placeholder would
938
- * snap to the destination default (often clipping off-canvas). We deep-clone the
939
- * effective source `a:xfrm` and write it explicitly onto the shape. Shapes with
940
- * their own `a:xfrm` are left untouched (explicit geometry is not inherited), and
941
- * an orphan placeholder with no source match keeps the current fall-back behaviour.
942
- */
943
- function resolvePlaceholderGeometry(slideRoot, ctx) {
944
- if (!ctx.layoutRoot && !ctx.masterRoot) return;
945
- for (const sp of elementsByTag(slideRoot, OOXML_NS.p, "sp")) {
946
- const ph = placeholderOf(sp);
947
- if (!ph) continue;
948
- const spPr = firstChild(sp, "p:spPr");
949
- if (spPr && firstChild(spPr, "a:xfrm")) continue;
950
- const found = placeholderInheritedXfrm(attr(ph, "type"), attr(ph, "idx") ?? "0", ctx);
951
- if (!found) continue;
952
- insertInOrder(getOrAddChild(sp, "p:spPr", SHAPE_AFTER_SPPR$1), found.xfrm.cloneNode(true), SPPR_XFRM_AFTER);
953
- }
954
- }
955
- /**
956
- * The `a:xfrm` a placeholder inherits from the source layout, then master,
957
- * tagged with which tier it came from — `null` when neither defines one. The
958
- * read-model {@link resolveInheritedFrame} (`theme-context.ts`) sibling of
959
- * {@link placeholderInheritedAnchor}; also the bake source for
960
- * {@link resolvePlaceholderGeometry}.
849
+ * The colour *element* a placeholder run inherits from the source style chain. Returns the
850
+ * first tier's colour element that resolves against `ctx` (the `a:srgbClr`/`a:schemeClr`/…
851
+ * inside its `a:solidFill`), or `null` when nothing in the chain defines a resolvable colour.
852
+ * The read-model `Run.resolvedColor` getter feeds this element to `resolveColorElement` for the
853
+ * `effectiveHex`; the flatten path resolves it directly via {@link placeholderInheritedColor}.
961
854
  */
962
- function placeholderInheritedXfrm(type, idx, ctx) {
963
- for (const [root, source] of [[ctx.layoutRoot, "layout"], [ctx.masterRoot, "master"]]) {
964
- if (!root) continue;
965
- const ph = findPlaceholder(root, type, idx);
966
- const spPr = ph && firstChild(ph, "p:spPr");
967
- const xfrm = spPr && firstChild(spPr, "a:xfrm");
968
- if (xfrm) return {
969
- xfrm,
970
- source
971
- };
972
- }
973
- return null;
974
- }
975
- /**
976
- * The vertical anchor (`a:bodyPr/@anchor`) a placeholder inherits from the source
977
- * layout, then master, or `null`. Resolves per-attribute: the first tier whose
978
- * placeholder `a:bodyPr` actually sets `@anchor` wins, so a layout `a:bodyPr`
979
- * present but without `@anchor` does not mask the master's. The read-model
980
- * {@link TextFrame.resolvedAnchor} sibling of {@link placeholderInheritedXfrm}.
981
- */
982
- function placeholderInheritedAnchor(type, idx, ctx) {
983
- for (const root of [ctx.layoutRoot, ctx.masterRoot]) {
984
- if (!root) continue;
985
- const ph = findPlaceholder(root, type, idx);
986
- const txBody = ph && firstChild(ph, "p:txBody");
987
- const bodyPr = txBody && firstChild(txBody, "a:bodyPr");
988
- const anchor = bodyPr && attr(bodyPr, "anchor");
989
- if (anchor) return anchor;
855
+ function placeholderInheritedFill(type, idx, level, ctx) {
856
+ for (const tier of styleTiers(type, idx, ctx)) {
857
+ const fill = lstStyleLevelFill(tier, level);
858
+ const colorEl = fill && firstChildElement(fill);
859
+ if (colorEl && resolveColor(colorEl, ctx)) return colorEl;
990
860
  }
991
861
  return null;
992
862
  }
993
863
  /**
994
- * Bake the placeholder-inherited vertical anchor onto a lifted shape's text body
995
- * (the {@link flattenShape} placeholder-demotion path). A placeholder whose own
996
- * `a:bodyPr` sets no `@anchor` inherits it from the source layout/master placeholder;
997
- * once {@link demotePlaceholders} strips the `p:ph` the shape would lose that
998
- * inheritance and fall back to top-anchored, so a vertically-centred title jumps.
999
- * We resolve the effective anchor from the source chain (via
1000
- * {@link placeholderInheritedAnchor}) and write it explicitly. Shapes whose
1001
- * `a:bodyPr` already fixes `@anchor`, and placeholders with no resolvable inherited
1002
- * anchor, are left untouched. Other `a:bodyPr` knobs (insets, autofit) are left as
1003
- * authored — they have sane defaults a plain shape keeps.
864
+ * The level `a:defRPr` elements a placeholder inherits from the source style chain, in
865
+ * resolution order. Tiers with no `a:defRPr` for `level` are dropped. The shared root for
866
+ * inherited run *size* and *typeface* resolution (the size/face sibling of
867
+ * {@link placeholderInheritedFill}), read directly by the flatten path and the read-model font
868
+ * getters.
1004
869
  */
1005
- function resolvePlaceholderBodyPr(shapeRoot, ctx) {
1006
- if (!ctx.layoutRoot && !ctx.masterRoot) return;
1007
- for (const sp of elementsByTag(shapeRoot, OOXML_NS.p, "sp")) {
1008
- const ph = placeholderOf(sp);
1009
- if (!ph) continue;
1010
- const txBody = firstChild(sp, "p:txBody");
1011
- if (!txBody) continue;
1012
- const bodyPr = firstChild(txBody, "a:bodyPr");
1013
- if (bodyPr && attr(bodyPr, "anchor") != null) continue;
1014
- const anchor = placeholderInheritedAnchor(attr(ph, "type"), attr(ph, "idx") ?? "0", ctx);
1015
- if (!anchor) continue;
1016
- setAttr(bodyPr ?? getOrAddChild(txBody, "a:bodyPr", ["a:lstStyle", "a:p"]), "anchor", anchor);
1017
- }
1018
- }
1019
- /** The ordered children of a `CT_TextListStyle` (`a:lstStyle` / a `p:txStyles` style). */
1020
- const LST_STYLE_LEVELS = [
1021
- "a:defPPr",
1022
- "a:lvl1pPr",
1023
- "a:lvl2pPr",
1024
- "a:lvl3pPr",
1025
- "a:lvl4pPr",
1026
- "a:lvl5pPr",
1027
- "a:lvl6pPr",
1028
- "a:lvl7pPr",
1029
- "a:lvl8pPr",
1030
- "a:lvl9pPr"
1031
- ];
1032
- /**
1033
- * Bake the placeholder-inherited list style onto a lifted shape's text body (the
1034
- * {@link flattenShape} placeholder-demotion path). A placeholder's per-level
1035
- * *paragraph* formatting — indent (`marL`/`indent`), bullets (`a:buChar`/
1036
- * `a:buAutoNum`/`a:buNone`), alignment, and the level `a:defRPr` — is inherited
1037
- * from the source layout/master placeholder `a:lstStyle` and the master
1038
- * `p:txStyles` category style. Run colour/size are baked onto each run by the
1039
- * sibling passes, but paragraph-level defaults live here; once
1040
- * {@link demotePlaceholders} strips the `p:ph` the shape stops inheriting them, so
1041
- * bullets and indents would vanish. We materialize the effective list style onto
1042
- * the shape's `p:txBody/a:lstStyle`.
1043
- *
1044
- * Resolution is per *level*, most specific tier wins whole: the slide's own level
1045
- * (if it defines one) is kept verbatim, else the first source tier that defines it
1046
- * (layout placeholder, then master placeholder, then master category style) is
1047
- * cloned. This is a whole-element overlay, not the per-attribute merge PowerPoint
1048
- * does — but explicit paragraph `a:pPr` on the slide's own runs travels with the
1049
- * shape and still wins, so this only supplies defaults for the inherited case
1050
- * (paragraphs that set no `a:pPr` of their own). Scheme colours in the cloned
1051
- * levels are resolved by the later {@link resolveSchemeColors} pass.
1052
- */
1053
- function resolvePlaceholderListStyle(shapeRoot, ctx) {
1054
- if (!ctx.layoutRoot && !ctx.masterRoot) return;
1055
- for (const sp of elementsByTag(shapeRoot, OOXML_NS.p, "sp")) {
1056
- const ph = placeholderOf(sp);
1057
- if (!ph) continue;
1058
- const txBody = firstChild(sp, "p:txBody");
1059
- if (!txBody) continue;
1060
- const tiers = placeholderInheritedListStyles(attr(ph, "type"), attr(ph, "idx") ?? "0", ctx);
1061
- if (tiers.length === 0) continue;
1062
- const slideLst = firstChild(txBody, "a:lstStyle");
1063
- const merged = createElement(ownerDocumentOf(txBody), "a:lstStyle");
1064
- let any = false;
1065
- for (const level of LST_STYLE_LEVELS) {
1066
- const src = (slideLst && firstChild(slideLst, level)) ?? tiers.map((t) => firstChild(t, level)).find((e) => !!e) ?? null;
1067
- if (!src) continue;
1068
- merged.appendChild(src.cloneNode(true));
1069
- any = true;
1070
- }
1071
- if (!any) continue;
1072
- if (slideLst) txBody.replaceChild(merged, slideLst);
1073
- else insertInOrder(txBody, merged, ["a:p"]);
1074
- }
1075
- }
1076
- /**
1077
- * The source list-style tiers a placeholder inherits paragraph formatting from,
1078
- * most specific first: layout placeholder `a:lstStyle`, master placeholder
1079
- * `a:lstStyle`, then the master `p:txStyles` category style (itself a
1080
- * `CT_TextListStyle`). The paragraph-level sibling of {@link placeholderInheritedDefRPrs}.
1081
- */
1082
- function placeholderInheritedListStyles(type, idx, ctx) {
1083
- const tiers = [];
1084
- if (ctx.layoutRoot) {
1085
- const layoutPh = findPlaceholder(ctx.layoutRoot, type, idx);
1086
- tiers.push(layoutPh && placeholderLstStyle(layoutPh));
1087
- }
1088
- if (ctx.masterRoot) {
1089
- const masterPh = findPlaceholder(ctx.masterRoot, type, idx);
1090
- tiers.push(masterPh && placeholderLstStyle(masterPh));
1091
- const txStyles = firstChild(ctx.masterRoot, "p:txStyles");
1092
- tiers.push(txStyles && firstChild(txStyles, TX_STYLE_NAME[phCategory(type)]));
1093
- }
1094
- return tiers.filter((t) => t !== null);
870
+ function placeholderInheritedDefRPrs(type, idx, level, ctx) {
871
+ return styleTiers(type, idx, ctx).map((tier) => lstStyleLevelDefRPr(tier, level)).filter((t) => t !== null);
1095
872
  }
1096
873
  /**
1097
- * Strip the `p:ph` marker from every placeholder shape in a lifted subtree so it
1098
- * becomes a self-contained ordinary shape (the {@link flattenShape} path only). By
1099
- * the time this runs, the shape's inherited geometry, colour, run size, anchor, and
1100
- * list style are all baked explicitly, so it no longer needs — and must not keep —
1101
- * its placeholder identity: a surviving `p:ph` would re-resolve against the *host*
1102
- * deck's layout/master placeholder of the same type/idx (wrong inheritance, or a
1103
- * fallback when the host has none) and could collide with the host slide's own
1104
- * placeholder of that type. Removing the marker severs both. The `p:nvPr` itself is
1105
- * kept (it can carry media/custom-data children); only the `p:ph` child goes.
874
+ * The source list-style tiers a placeholder inherits paragraph formatting from, most specific
875
+ * first. The paragraph-level sibling of {@link placeholderInheritedDefRPrs} — it hands back the
876
+ * tier elements whole rather than pulling one level's properties out of each.
877
+ *
878
+ * The notes tier is included for consistency with the rest of the chain, though it cannot arise
879
+ * in practice: the only caller is the list-style bake in `flattenShape`, which runs on slide and
880
+ * shape contexts, and `ctx.notesStyle` is set only on a notes context.
1106
881
  */
1107
- function demotePlaceholders(shapeRoot) {
1108
- for (const sp of elementsByTag(shapeRoot, OOXML_NS.p, "sp")) {
1109
- const nvSpPr = firstChild(sp, "p:nvSpPr");
1110
- const nvPr = nvSpPr && firstChild(nvSpPr, "p:nvPr");
1111
- if (nvPr) removeChildrenByQName(nvPr, ["p:ph"]);
1112
- }
882
+ function placeholderInheritedListStyles(type, idx, ctx) {
883
+ return styleTiers(type, idx, ctx);
1113
884
  }
1114
- /** Inheritable run properties baked under `preserve`: size and weight/slant (not typeface). */
1115
- const RUN_PROP_NAMES = [
1116
- "sz",
1117
- "b",
1118
- "i"
1119
- ];
1120
885
  /**
1121
- * Bake placeholder-inherited run size/weight onto the slide (gap 3). Mirrors
1122
- * {@link resolvePlaceholderRunColors}: for each placeholder run that sets no
1123
- * `sz`/`b`/`i` of its own (nor at paragraph/text-body level on the *slide*),
1124
- * resolve the value it would inherit from the source `slideLayout`/`slideMaster`
1125
- * text styles — per paragraph list level — and write it explicitly onto the run's
1126
- * `a:rPr`. Each property resolves independently up the chain. Typeface (`a:latin`)
1127
- * is left to re-bind to the destination theme, as gap 1 does for the `fontRef`.
886
+ * The colour a placeholder run inherits from the source style chain, resolved to a
887
+ * literal `{ hex, transforms }`. Thin wrapper over {@link placeholderInheritedFill}
888
+ * for the flatten path, which re-emits the transforms verbatim. Returns `null`
889
+ * when nothing in the chain defines a resolvable colour (the run then re-binds to
890
+ * the destination).
1128
891
  */
1129
- function resolvePlaceholderRunSizes(slideRoot, ctx) {
1130
- if (!ctx.layoutRoot && !ctx.masterRoot) return;
1131
- for (const sp of elementsByTag(slideRoot, OOXML_NS.p, "sp")) {
1132
- const ph = placeholderOf(sp);
1133
- if (!ph) continue;
1134
- const txBody = firstChild(sp, "p:txBody");
1135
- if (!txBody) continue;
1136
- const type = attr(ph, "type");
1137
- const idx = attr(ph, "idx") ?? "0";
1138
- const slideLst = firstChild(txBody, "a:lstStyle");
1139
- const byLevel = /* @__PURE__ */ new Map();
1140
- for (const p of getElements(txBody, "a:p")) {
1141
- const pPr = firstChild(p, "a:pPr");
1142
- const level = (pPr && intValue(attr(pPr, "lvl"))) ?? 0;
1143
- const runs = [...getElements(p, "a:r"), ...getElements(p, "a:fld")];
1144
- if (runs.length === 0) continue;
1145
- let props = byLevel.get(level);
1146
- if (props === void 0) {
1147
- props = placeholderInheritedRunProps(type, idx, level, ctx);
1148
- byLevel.set(level, props);
1149
- }
1150
- if (!props) continue;
1151
- for (const run of runs) writeRunProps(run, props, pPr, slideLst, level);
1152
- }
1153
- }
892
+ function placeholderInheritedColor(type, idx, level, ctx) {
893
+ const colorEl = placeholderInheritedFill(type, idx, level, ctx);
894
+ return colorEl ? resolveColor(colorEl, ctx) : null;
1154
895
  }
1155
896
  /**
1156
- * The run size/weight a placeholder run inherits from the source style chain:
1157
- * layout placeholder `a:lstStyle` → master placeholder `a:lstStyle` → master
1158
- * `p:txStyles` category style, per list level. Each of `sz`/`b`/`i` is taken from
1159
- * the first tier that defines it (properties resolve independently). Returns
1160
- * `null` when no tier defines any of them.
897
+ * The run size/weight a placeholder run inherits from the source style chain, per list level.
898
+ * Each of `sz`/`b`/`i` is taken from the first tier that defines it (properties resolve
899
+ * independently). Returns `null` when no tier defines any of them.
1161
900
  */
1162
901
  function placeholderInheritedRunProps(type, idx, level, ctx) {
1163
902
  const tiers = placeholderInheritedDefRPrs(type, idx, level, ctx);
@@ -1174,141 +913,60 @@ function placeholderInheritedRunProps(type, idx, level, ctx) {
1174
913
  }
1175
914
  return any ? props : null;
1176
915
  }
1177
- /** Whether the *slide itself* already fixes a run property (so a rebind cannot change it). */
1178
- function slideDefinesProp(name, run, pPr, slideLst, level) {
1179
- const rPr = firstChild(run, "a:rPr");
1180
- if (rPr && attr(rPr, name) != null) return true;
1181
- const defRPr = pPr && firstChild(pPr, "a:defRPr");
1182
- if (defRPr && attr(defRPr, name) != null) return true;
1183
- const slideDefRPr = lstStyleLevelDefRPr(slideLst, level);
1184
- return !!(slideDefRPr && attr(slideDefRPr, name) != null);
1185
- }
1186
- /** Write each resolved run property onto a run's `a:rPr`, skipping ones the slide already fixes. */
1187
- function writeRunProps(run, props, pPr, slideLst, level) {
1188
- let rPr = null;
1189
- for (const name of RUN_PROP_NAMES) {
1190
- const value = props[name];
1191
- if (value == null) continue;
1192
- if (slideDefinesProp(name, run, pPr, slideLst, level)) continue;
1193
- rPr ??= getOrAddChild(run, "a:rPr", ["a:t"]);
1194
- setAttr(rPr, name, value);
1195
- }
1196
- }
1197
- /** Snapshot all descendant elements of `root` matching a namespace + local name. */
1198
- function elementsByTag(root, ns, local) {
1199
- const out = [];
1200
- for (const el of root.getElementsByTagNameNS(ns, local)) out.push(el);
1201
- return out;
1202
- }
1203
916
  /**
1204
- * Resolve each shape's `p:style` `lnRef`/`fillRef`/`effectRef` into explicit
1205
- * `spPr` children (using the theme `fmtScheme`), then neutralize the ref. The
1206
- * `fontRef` is intentionally left for the destination theme to re-resolve.
917
+ * The `a:xfrm` a placeholder inherits from the source layout, then master,
918
+ * tagged with which tier it came from — `null` when neither defines one. The
919
+ * read-model `resolveInheritedFrame` (`theme-context.ts`) sibling of
920
+ * {@link placeholderInheritedAnchor}; also the bake source for the flatten pass'
921
+ * `resolvePlaceholderGeometry`.
1207
922
  */
1208
- function materializeStyleRefs(root, ctx) {
1209
- if (!ctx.fmtScheme) return;
1210
- for (const style of elementsByTag(root, OOXML_NS.p, "style")) {
1211
- const shape = style.parentNode;
1212
- if (!shape) continue;
1213
- const spPr = getOrAddChild(shape, "p:spPr", SHAPE_AFTER_SPPR$1);
1214
- materializeFill(spPr, firstChild(style, "a:fillRef"), ctx);
1215
- materializeLine(spPr, firstChild(style, "a:lnRef"), ctx);
1216
- materializeEffect(spPr, firstChild(style, "a:effectRef"), ctx);
1217
- }
1218
- }
1219
- /** Replace every `phClr` under `el` with the ref colour (ref transforms first, then the `phClr`'s own). */
1220
- function substitutePhClr(el, ref) {
1221
- const doc = ownerDocumentOf(el);
1222
- for (const phClr of elementsByTag(el, OOXML_NS.a, "schemeClr")) {
1223
- if (attr(phClr, "val") !== "phClr") continue;
1224
- const srgb = createElement(doc, "a:srgbClr");
1225
- setAttr(srgb, "val", ref.hex);
1226
- for (const t of ref.transforms) srgb.appendChild(t.cloneNode(true));
1227
- while (phClr.firstChild) srgb.appendChild(phClr.firstChild);
1228
- replaceInParent(phClr, srgb);
923
+ function placeholderInheritedXfrm(type, idx, ctx) {
924
+ for (const [root, source] of [[ctx.layoutRoot, "layout"], [ctx.masterRoot, "master"]]) {
925
+ if (!root) continue;
926
+ const ph = findPlaceholder(root, type, idx);
927
+ const spPr = ph && firstChild(ph, "p:spPr");
928
+ const xfrm = spPr && firstChild(spPr, "a:xfrm");
929
+ if (xfrm) return {
930
+ xfrm,
931
+ source
932
+ };
1229
933
  }
1230
- }
1231
- /** The `idx`-th entry (1-based) of a `fmtScheme` style list, deep-cloned, or `null`. */
1232
- function fmtEntry(ctx, listName, idx) {
1233
- const list = ctx.fmtScheme && firstChild(ctx.fmtScheme, listName);
1234
- if (!list || idx < 1) return null;
1235
- const entry = childElements$1(list)[idx - 1];
1236
- return entry ? entry.cloneNode(true) : null;
1237
- }
1238
- /**
1239
- * Build the explicit fill element a `p:style` `a:fillRef` resolves to — the indexed
1240
- * `fmtScheme` `fillStyleLst`/`bgFillStyleLst` entry (deep-cloned) with its `phClr`
1241
- * replaced by the ref's resolved colour. Pure: mutates neither the ref nor the
1242
- * theme. `null` when the ref is absent, `idx` is 0/unset, or the entry or its
1243
- * colour cannot be resolved. Shared by the flatten path ({@link materializeFill})
1244
- * and the read-model `resolveStyleFillColor` getter so both see the same fill.
1245
- */
1246
- function styleRefFill(fillRef, ctx) {
1247
- if (!fillRef) return null;
1248
- const idx = intAttr(fillRef, "idx");
1249
- if (idx === null || idx <= 0) return null;
1250
- const fill = idx >= 1e3 ? fmtEntry(ctx, "a:bgFillStyleLst", idx - 1e3) : fmtEntry(ctx, "a:fillStyleLst", idx);
1251
- const ref = resolveColor(firstChildElement(fillRef), ctx);
1252
- if (!fill || !ref) return null;
1253
- substitutePhClr(fill, ref);
1254
- return fill;
934
+ return null;
1255
935
  }
1256
936
  /**
1257
- * Build the explicit `a:ln` element a `p:style` `a:lnRef` resolves to — the indexed
1258
- * `fmtScheme` `lnStyleLst` entry (deep-cloned) with its `phClr` replaced by the
1259
- * ref's resolved colour. Pure; the line/read counterpart of {@link styleRefFill}.
937
+ * The vertical anchor (`a:bodyPr/@anchor`) a placeholder inherits from the source
938
+ * layout, then master, or `null`. Resolves per-attribute: the first tier whose
939
+ * placeholder `a:bodyPr` actually sets `@anchor` wins, so a layout `a:bodyPr`
940
+ * present but without `@anchor` does not mask the master's. The read-model
941
+ * `TextFrame.resolvedAnchor` sibling of {@link placeholderInheritedXfrm}.
1260
942
  */
1261
- function styleRefLine(lnRef, ctx) {
1262
- if (!lnRef) return null;
1263
- const idx = intAttr(lnRef, "idx");
1264
- if (idx === null || idx <= 0) return null;
1265
- const ln = fmtEntry(ctx, "a:lnStyleLst", idx);
1266
- const ref = resolveColor(firstChildElement(lnRef), ctx);
1267
- if (!ln || !ref) return null;
1268
- substitutePhClr(ln, ref);
1269
- return ln;
1270
- }
1271
- function materializeFill(spPr, fillRef, ctx) {
1272
- if (!fillRef) return;
1273
- if (!FILL_CHOICES.some((q) => firstChild(spPr, q))) {
1274
- const fill = styleRefFill(fillRef, ctx);
1275
- if (fill) insertInOrder(spPr, fill, SPPR_FILL_AFTER$1);
1276
- }
1277
- neutralizeRef(fillRef);
1278
- }
1279
- function materializeLine(spPr, lnRef, ctx) {
1280
- if (!lnRef) return;
1281
- if (!firstChild(spPr, "a:ln")) {
1282
- const ln = styleRefLine(lnRef, ctx);
1283
- if (ln) insertInOrder(spPr, ln, SPPR_LN_AFTER$1);
1284
- }
1285
- neutralizeRef(lnRef);
1286
- }
1287
- function materializeEffect(spPr, effectRef, ctx) {
1288
- if (!effectRef) return;
1289
- const idx = intAttr(effectRef, "idx");
1290
- if (idx !== null && idx > 0 && !firstChild(spPr, "a:effectLst") && !firstChild(spPr, "a:effectDag")) {
1291
- const style = fmtEntry(ctx, "a:effectStyleLst", idx);
1292
- const ref = resolveColor(firstChildElement(effectRef), ctx);
1293
- if (style && ref) {
1294
- substitutePhClr(style, ref);
1295
- for (const child of childElements$1(style)) if (isA(child, "effectLst") || isA(child, "effectDag")) insertInOrder(spPr, child, SPPR_EFFECT_AFTER);
1296
- else if (isA(child, "scene3d")) insertInOrder(spPr, child, ["a:sp3d", "a:extLst"]);
1297
- else if (isA(child, "sp3d")) insertInOrder(spPr, child, ["a:extLst"]);
1298
- }
943
+ function placeholderInheritedAnchor(type, idx, ctx) {
944
+ for (const root of [ctx.layoutRoot, ctx.masterRoot]) {
945
+ if (!root) continue;
946
+ const ph = findPlaceholder(root, type, idx);
947
+ const txBody = ph && firstChild(ph, "p:txBody");
948
+ const bodyPr = txBody && firstChild(txBody, "a:bodyPr");
949
+ const anchor = bodyPr && attr(bodyPr, "anchor");
950
+ if (anchor) return anchor;
1299
951
  }
1300
- neutralizeRef(effectRef);
952
+ return null;
1301
953
  }
1302
- /** Strip a style-matrix ref to `idx="0"` with no colour child so it contributes nothing. */
1303
- function neutralizeRef(ref) {
1304
- setAttr(ref, "idx", "0");
1305
- for (const child of childElements$1(ref)) ref.removeChild(child);
954
+ /** Whether the *slide itself* already fixes this run's colour (so a rebind cannot change it). */
955
+ function slideDefinesColor(run, pPr, slideLst, level) {
956
+ const rPr = firstChild(run, "a:rPr");
957
+ if (rPr && FILL_CHOICES.some((q) => firstChild(rPr, q))) return true;
958
+ const defRPr = pPr && firstChild(pPr, "a:defRPr");
959
+ if (defRPr && firstChild(defRPr, "a:solidFill")) return true;
960
+ return !!lstStyleLevelFill(slideLst, level);
1306
961
  }
1307
- function intAttr(el, name) {
1308
- const value = attr(el, name);
1309
- if (value === null || value === "") return null;
1310
- const n = Number(value);
1311
- return Number.isFinite(n) ? n : null;
962
+ /** Whether the *slide itself* already fixes a run property (so a rebind cannot change it). */
963
+ function slideDefinesProp(name, run, pPr, slideLst, level) {
964
+ const rPr = firstChild(run, "a:rPr");
965
+ if (rPr && attr(rPr, name) != null) return true;
966
+ const defRPr = pPr && firstChild(pPr, "a:defRPr");
967
+ if (defRPr && attr(defRPr, name) != null) return true;
968
+ const slideDefRPr = lstStyleLevelDefRPr(slideLst, level);
969
+ return !!(slideDefRPr && attr(slideDefRPr, name) != null);
1312
970
  }
1313
971
  //#endregion
1314
972
  //#region src/read/oxml/color-transform.ts
@@ -1484,7 +1142,6 @@ function applyColorTransforms(baseHex, transforms) {
1484
1142
  if (f !== null) alpha = clamp01((alpha ?? 1) + f);
1485
1143
  break;
1486
1144
  }
1487
- default: break;
1488
1145
  }
1489
1146
  return alpha === void 0 ? { hex: toHex(rgb) } : {
1490
1147
  hex: toHex(rgb),
@@ -1504,17 +1161,6 @@ function applyColorTransforms(baseHex, transforms) {
1504
1161
  * background needs on top of {@link resolveSlideThemeParts} — so a token resolves
1505
1162
  * identically whether it is read or baked.
1506
1163
  */
1507
- const SLIDE_LAYOUT_REL = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideLayout";
1508
- const SLIDE_MASTER_REL = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideMaster";
1509
- const NOTES_MASTER_REL = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/notesMaster";
1510
- const THEME_REL = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme";
1511
- const OFFICE_DOCUMENT_REL = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument";
1512
- /** Resolve the single relationship of `type` owned by `partName`, or `null`. */
1513
- function resolveSingleRel(opc, partName, type) {
1514
- const rels = opc.relationshipsFor(partName);
1515
- const rel = rels.byType(type)[0];
1516
- return rel ? rels.resolveTarget(rel.id) : null;
1517
- }
1518
1164
  /** The document element of a part, or `null` when the partname/part is absent. */
1519
1165
  function documentElement(opc, partName) {
1520
1166
  return partName ? opc.part(partName)?.dom.documentElement ?? null : null;
@@ -1523,7 +1169,7 @@ function documentElement(opc, partName) {
1523
1169
  * The presentation's `p:defaultTextStyle` (`presentation.xml`, reached via the
1524
1170
  * package `officeDocument` relationship), or `null` when absent. This is
1525
1171
  * PowerPoint's lowest-priority text fallback — the bottom tier of a slide run's
1526
- * size/colour/face/bold resolution (see `FlattenContext.defaultTextStyle`).
1172
+ * size/colour/face/bold resolution (see `ThemeContext.defaultTextStyle`).
1527
1173
  */
1528
1174
  function presentationDefaultTextStyle(opc) {
1529
1175
  const root = documentElement(opc, resolveSingleRel(opc, "/", OFFICE_DOCUMENT_REL));
@@ -1999,112 +1645,203 @@ function readPictureFill(container, rels = null) {
1999
1645
  const fillRect = stretch && firstChild(stretch, "a:fillRect");
2000
1646
  const tile = firstChild(blipFill, "a:tile");
2001
1647
  return {
2002
- relId,
2003
- partName: resolvePartName(relId, rels),
2004
- mode: stretch ? "stretch" : tile ? "tile" : null,
2005
- srcRect: srcRect ? readRect(srcRect) : null,
2006
- fillRect: fillRect ? readRect(fillRect) : null,
2007
- tile: tile ? readTile(tile) : null,
2008
- alpha: alphaMod ? amt === null ? 1 : amt / 1e5 : null,
2009
- dpi: intValue(attr(blipFill, "dpi")),
2010
- rotWithShape: boolValue(attr(blipFill, "rotWithShape"))
1648
+ relId,
1649
+ partName: resolvePartName(relId, rels),
1650
+ mode: stretch ? "stretch" : tile ? "tile" : null,
1651
+ srcRect: srcRect ? readRect(srcRect) : null,
1652
+ fillRect: fillRect ? readRect(fillRect) : null,
1653
+ tile: tile ? readTile(tile) : null,
1654
+ alpha: alphaMod ? amt === null ? 1 : amt / 1e5 : null,
1655
+ dpi: intValue(attr(blipFill, "dpi")),
1656
+ rotWithShape: boolValue(attr(blipFill, "rotWithShape"))
1657
+ };
1658
+ }
1659
+ //#endregion
1660
+ //#region src/read/api/pattern-fill.ts
1661
+ /**
1662
+ * Shared pattern-fill reader for DrawingML colour-bearing containers: a shape's `p:spPr`, a
1663
+ * slide's `p:bg/p:bgPr`, and a table's `a:tblPr` / cell's `a:tcPr`.
1664
+ *
1665
+ * `a:pattFill` is a preset hatch name plus two wrapped colours, and decoding it is three
1666
+ * lines — which is exactly why it had been open-coded in each place that needed it. Sharing
1667
+ * it is what keeps the three from drifting into different answers for the same element, the
1668
+ * same reason {@link import('./gradient.js').readGradientFill} and
1669
+ * {@link import('./picture-fill.js').readPictureFill} are shared.
1670
+ */
1671
+ /**
1672
+ * Read a container's `a:pattFill`, or `null` when its fill is not a pattern.
1673
+ * @param {Element} container - the fill-bearing parent (`p:spPr`, `p:bgPr`, `a:tblPr`, `a:tcPr`, …)
1674
+ * @param {ColorContext} ctx - the theme colour context each colour resolves against
1675
+ * @returns {PatternFill | null} the decoded hatch, or `null`
1676
+ */
1677
+ function readPatternFill(container, ctx) {
1678
+ const patt = firstChild(container, "a:pattFill");
1679
+ if (!patt) return null;
1680
+ const wrapColor = (qname) => {
1681
+ const wrap = firstChild(patt, qname);
1682
+ const colorEl = wrap && firstChildElement(wrap);
1683
+ return colorEl ? resolveColorElement(colorEl, ctx) : null;
1684
+ };
1685
+ return {
1686
+ preset: attr(patt, "prst") ?? null,
1687
+ foreground: wrapColor("a:fgClr"),
1688
+ background: wrapColor("a:bgClr")
1689
+ };
1690
+ }
1691
+ //#endregion
1692
+ //#region src/read/api/shapes/oxml.ts
1693
+ /**
1694
+ * Shape-element plumbing: the small element lookups every shape kind performs, plus the
1695
+ * shape-scoped re-exports of the shared schema-successor lists.
1696
+ *
1697
+ * The successor arrays are the reason a setter can create a missing `a:xfrm` / `a:solidFill` /
1698
+ * `a:ln` without corrupting the part: `getOrAddChild` inserts before the first listed sibling it
1699
+ * finds, so each list must name exactly the children that legally *follow* the one being added.
1700
+ * They are declared once in `src/ooxml/sequence.ts` (derived from each complexType's sequence)
1701
+ * and re-exported here so the shape modules keep importing them from one place.
1702
+ */
1703
+ /** First `<p:cNvPr>` reached through the shape's non-visual properties wrapper (`p:nv*Pr`). */
1704
+ function nonVisualCNvPr(element) {
1705
+ for (let node = element.firstChild; node; node = node.nextSibling) {
1706
+ if (node.nodeType !== 1) continue;
1707
+ const child = node;
1708
+ if (child.namespaceURI === OOXML_NS.p && child.localName?.startsWith("nv")) return firstChild(child, "p:cNvPr");
1709
+ }
1710
+ return null;
1711
+ }
1712
+ function emuFrom(parent, qname, attribute) {
1713
+ const element = parent && firstChild(parent, qname);
1714
+ return element ? intValue(attr(element, attribute)) : null;
1715
+ }
1716
+ /**
1717
+ * One EMU coordinate from a shape's own `p:spPr/a:xfrm`, or `null` when any link in that chain
1718
+ * is absent — which is how an inheriting placeholder (one with no `a:xfrm` of its own) reports
1719
+ * that its geometry comes from the layout/master instead. See `placeholderInheritedXfrm` for
1720
+ * resolving what it would inherit.
1721
+ */
1722
+ function spPrXfrmEmu(sp, container, axis) {
1723
+ const spPr = firstChild(sp, "p:spPr");
1724
+ const xfrm = spPr && firstChild(spPr, "a:xfrm");
1725
+ const el = xfrm && firstChild(xfrm, container);
1726
+ return el ? intValue(attr(el, axis)) : null;
1727
+ }
1728
+ /** Get-or-add `p:spPr/a:xfrm` for shapes whose transform lives in `p:spPr` (`p:sp`, `p:pic`, `p:cxnSp`). */
1729
+ function getOrAddSpPrXfrm(shapeElement) {
1730
+ return getOrAddChild(getOrAddChild(shapeElement, "p:spPr", SHAPE_AFTER_SPPR), "a:xfrm", SPPR_AFTER_XFRM);
1731
+ }
1732
+ //#endregion
1733
+ //#region src/read/api/shapes/geometry.ts
1734
+ /**
1735
+ * Reading geometry out of a shape: transform boxes, rotation and flips, and custom-geometry paths.
1736
+ *
1737
+ * Every value here comes straight off `a:xfrm` / `a:path` attributes, so these are pure element
1738
+ * readers with no theme or relationship context.
1739
+ */
1740
+ /**
1741
+ * One `a:pt` coordinate as a raw path-unit integer. A guide-name reference
1742
+ * (the `ST_AdjCoordinate` string form) is not produced by authored freeforms;
1743
+ * a non-numeric value degrades to `0` rather than crashing (documented edge).
1744
+ */
1745
+ function ptAxis(pt, axis) {
1746
+ return (pt ? intValue(attr(pt, axis)) : null) ?? 0;
1747
+ }
1748
+ /** Parse one `<a:path>` into its viewport attrs (with schema defaults) and ordered segments. */
1749
+ function readGeometryPath(path) {
1750
+ const commands = [];
1751
+ for (let node = path.firstChild; node; node = node.nextSibling) {
1752
+ if (node.nodeType !== 1) continue;
1753
+ const seg = node;
1754
+ if (seg.namespaceURI !== OOXML_NS.a) continue;
1755
+ const pts = getElements(seg, "a:pt");
1756
+ switch (seg.localName) {
1757
+ case "moveTo":
1758
+ commands.push({
1759
+ cmd: "moveTo",
1760
+ x: ptAxis(pts[0], "x"),
1761
+ y: ptAxis(pts[0], "y")
1762
+ });
1763
+ break;
1764
+ case "lnTo":
1765
+ commands.push({
1766
+ cmd: "lnTo",
1767
+ x: ptAxis(pts[0], "x"),
1768
+ y: ptAxis(pts[0], "y")
1769
+ });
1770
+ break;
1771
+ case "cubicBezTo":
1772
+ commands.push({
1773
+ cmd: "cubicBezTo",
1774
+ x1: ptAxis(pts[0], "x"),
1775
+ y1: ptAxis(pts[0], "y"),
1776
+ x2: ptAxis(pts[1], "x"),
1777
+ y2: ptAxis(pts[1], "y"),
1778
+ x: ptAxis(pts[2], "x"),
1779
+ y: ptAxis(pts[2], "y")
1780
+ });
1781
+ break;
1782
+ case "quadBezTo":
1783
+ commands.push({
1784
+ cmd: "quadBezTo",
1785
+ x1: ptAxis(pts[0], "x"),
1786
+ y1: ptAxis(pts[0], "y"),
1787
+ x: ptAxis(pts[1], "x"),
1788
+ y: ptAxis(pts[1], "y")
1789
+ });
1790
+ break;
1791
+ case "arcTo":
1792
+ commands.push({
1793
+ cmd: "arcTo",
1794
+ wR: intValue(attr(seg, "wR")) ?? 0,
1795
+ hR: intValue(attr(seg, "hR")) ?? 0,
1796
+ stAng: (intValue(attr(seg, "stAng")) ?? 0) / 6e4,
1797
+ swAng: (intValue(attr(seg, "swAng")) ?? 0) / 6e4
1798
+ });
1799
+ break;
1800
+ case "close": commands.push({ cmd: "close" });
1801
+ }
1802
+ }
1803
+ return {
1804
+ w: intValue(attr(path, "w")) ?? 0,
1805
+ h: intValue(attr(path, "h")) ?? 0,
1806
+ fill: attr(path, "fill") ?? "norm",
1807
+ stroke: boolValue(attr(path, "stroke")) ?? true,
1808
+ commands
2011
1809
  };
2012
1810
  }
2013
- //#endregion
2014
- //#region src/read/api/shapes/oxml.ts
2015
- /**
2016
- * Shape-element plumbing: the schema-successor lists that keep a get-or-added child in document
2017
- * order, and the small element lookups every shape kind performs.
2018
- *
2019
- * The successor arrays are the reason a setter can create a missing `a:xfrm` / `a:solidFill` /
2020
- * `a:ln` without corrupting the part: `getOrAddChild` inserts before the first listed sibling it
2021
- * finds, so each list must name exactly the children that legally *follow* the one being added.
2022
- */
2023
- const SPPR_AFTER_XFRM = [
2024
- "a:custGeom",
2025
- "a:prstGeom",
2026
- "a:noFill",
2027
- "a:solidFill",
2028
- "a:gradFill",
2029
- "a:blipFill",
2030
- "a:pattFill",
2031
- "a:grpFill",
2032
- "a:ln",
2033
- "a:effectLst",
2034
- "a:effectDag",
2035
- "a:scene3d",
2036
- "a:sp3d",
2037
- "a:extLst"
2038
- ];
2039
- const GRPSPPR_AFTER_XFRM = [
2040
- "a:noFill",
2041
- "a:solidFill",
2042
- "a:gradFill",
2043
- "a:blipFill",
2044
- "a:pattFill",
2045
- "a:grpFill",
2046
- "a:effectLst",
2047
- "a:effectDag",
2048
- "a:scene3d",
2049
- "a:extLst"
2050
- ];
2051
- const SHAPE_AFTER_SPPR = ["p:style", "p:txBody"];
2052
- const SPPR_FILL_AFTER = [
2053
- "a:ln",
2054
- "a:effectLst",
2055
- "a:effectDag",
2056
- "a:scene3d",
2057
- "a:sp3d",
2058
- "a:extLst"
2059
- ];
2060
- const SPPR_LN_AFTER = [
2061
- "a:effectLst",
2062
- "a:effectDag",
2063
- "a:scene3d",
2064
- "a:sp3d",
2065
- "a:extLst"
2066
- ];
2067
- const GRPSPPR_FILL_AFTER = [
2068
- "a:effectLst",
2069
- "a:effectDag",
2070
- "a:scene3d",
2071
- "a:extLst"
2072
- ];
2073
- const LN_FILL_AFTER = [
2074
- "a:prstDash",
2075
- "a:custDash",
2076
- "a:round",
2077
- "a:bevel",
2078
- "a:miter",
2079
- "a:headEnd",
2080
- "a:tailEnd",
2081
- "a:extLst"
2082
- ];
2083
- /** First `<p:cNvPr>` reached through the shape's non-visual properties wrapper (`p:nv*Pr`). */
2084
- function nonVisualCNvPr(element) {
2085
- for (let node = element.firstChild; node; node = node.nextSibling) {
2086
- if (node.nodeType !== 1) continue;
2087
- const child = node;
2088
- if (child.namespaceURI === OOXML_NS.p && child.localName?.startsWith("nv")) return firstChild(child, "p:cNvPr");
2089
- }
2090
- return null;
1811
+ /** Validate and round an EMU geometry value; extents (`cx`/`cy`) must be non-negative. */
1812
+ function toEmu(value, attribute, allowNegative) {
1813
+ if (!Number.isFinite(value)) throw new InvalidOptionError("coord/non-finite", `${attribute} must be a finite number of EMU, got ${value}`);
1814
+ if (!allowNegative && value < 0) throw new InvalidOptionError("coord/negative", `${attribute} must be non-negative, got ${value}`);
1815
+ return Math.round(value);
2091
1816
  }
2092
- function emuFrom(parent, qname, attribute) {
2093
- const element = parent && firstChild(parent, qname);
2094
- return element ? intValue(attr(element, attribute)) : null;
1817
+ /** A point + extent pair (`a:off`/`a:ext` or `a:chOff`/`a:chExt`) from a transform, or `null` if either is incomplete. */
1818
+ function readBox(xfrm, offName, extName) {
1819
+ const off = firstChild(xfrm, offName);
1820
+ const ext = firstChild(xfrm, extName);
1821
+ const x = off && intValue(attr(off, "x"));
1822
+ const y = off && intValue(attr(off, "y"));
1823
+ const cx = ext && intValue(attr(ext, "cx"));
1824
+ const cy = ext && intValue(attr(ext, "cy"));
1825
+ if (x === null || y === null || cx === null || cy === null) return null;
1826
+ return {
1827
+ x,
1828
+ y,
1829
+ cx,
1830
+ cy
1831
+ };
2095
1832
  }
2096
- /** Direct child *elements* of `parent`, in document order. */
2097
- function childElements(parent) {
2098
- const out = [];
2099
- for (let node = parent.firstChild; node; node = node.nextSibling) if (node.nodeType === 1) out.push(node);
2100
- return out;
1833
+ function rotationDegrees(xfrm) {
1834
+ const rot = intValue(attr(xfrm, "rot"));
1835
+ return rot === null ? 0 : rot / 6e4;
2101
1836
  }
2102
- /** Get-or-add `p:spPr/a:xfrm` for shapes whose transform lives in `p:spPr` (`p:sp`, `p:pic`, `p:cxnSp`). */
2103
- function getOrAddSpPrXfrm(shapeElement) {
2104
- return getOrAddChild(getOrAddChild(shapeElement, "p:spPr", SHAPE_AFTER_SPPR), "a:xfrm", SPPR_AFTER_XFRM);
1837
+ function transformFlipH(xfrm) {
1838
+ return boolValue(attr(xfrm, "flipH")) === true;
1839
+ }
1840
+ function transformFlipV(xfrm) {
1841
+ return boolValue(attr(xfrm, "flipV")) === true;
2105
1842
  }
2106
1843
  //#endregion
2107
- //#region src/read/group-transform.ts
1844
+ //#region src/read/api/shapes/group-transform.ts
2108
1845
  /** Normalise a degree value into [0, 360). */
2109
1846
  function normalizeDegrees(value) {
2110
1847
  return (value % 360 + 360) % 360;
@@ -2633,22 +2370,68 @@ var Paragraph = class {
2633
2370
  return this.#emuAttrPt("indent");
2634
2371
  }
2635
2372
  /**
2636
- * Bullet description for this paragraph, derived from the `a:pPr` bullet
2637
- * children, or `null` when unset (inherited from the list style):
2638
- * - `'none'` — explicit `a:buNone` (bullet suppressed)
2639
- * - `'char:•'` — `a:buChar/@char` (the literal glyph follows the colon)
2640
- * - `'autoNum:arabicPeriod'` — `a:buAutoNum/@type` (auto-numbered)
2373
+ * This paragraph's bullet — its kind, and for a drawn bullet the glyph, the
2374
+ * numbering start, and the bullet's own font/size/colour. `null` when the
2375
+ * paragraph names no bullet at all and inherits one from the list style.
2376
+ *
2377
+ * Structured rather than a tagged string, which is not merely tidier. The
2378
+ * previous `bullet` accessor reported `'none'` / `'char:•'` /
2379
+ * `'autoNum:arabicPeriod'`, and that shape is ambiguous when the glyph is
2380
+ * itself a colon; worse, it reads as a bare glyph, which is exactly how the
2381
+ * script converter first consumed it — `'none'.codePointAt(0)` put a literal
2382
+ * `n` bullet on every converted deck, silently. See {@link BulletDetail}.
2641
2383
  */
2642
- get bullet() {
2384
+ get bulletDetail() {
2643
2385
  const pPr = firstChild(this.element, "a:pPr");
2644
2386
  if (!pPr) return null;
2645
- if (firstChild(pPr, "a:buNone")) return "none";
2387
+ if (firstChild(pPr, "a:buNone")) return { kind: "none" };
2388
+ const style = this.#bulletStyle(pPr);
2646
2389
  const buChar = firstChild(pPr, "a:buChar");
2647
- if (buChar) return `char:${attr(buChar, "char") ?? ""}`;
2390
+ if (buChar) return {
2391
+ kind: "char",
2392
+ char: attr(buChar, "char") ?? "",
2393
+ ...style
2394
+ };
2648
2395
  const buAutoNum = firstChild(pPr, "a:buAutoNum");
2649
- if (buAutoNum) return `autoNum:${attr(buAutoNum, "type") ?? ""}`;
2396
+ if (buAutoNum) return {
2397
+ kind: "autoNum",
2398
+ scheme: attr(buAutoNum, "type") ?? "",
2399
+ startAt: intValue(attr(buAutoNum, "startAt")),
2400
+ ...style
2401
+ };
2402
+ const buBlip = firstChild(pPr, "a:buBlip");
2403
+ if (buBlip) return {
2404
+ kind: "picture",
2405
+ imagePartName: this.#bulletImagePartName(buBlip),
2406
+ ...style
2407
+ };
2650
2408
  return null;
2651
2409
  }
2410
+ /** The `a:buFont` / `a:buSz*` / `a:buClr` siblings of a `a:pPr` bullet choice. */
2411
+ #bulletStyle(pPr) {
2412
+ const buFont = firstChild(pPr, "a:buFont");
2413
+ const buSzPct = firstChild(pPr, "a:buSzPct");
2414
+ const buSzPts = firstChild(pPr, "a:buSzPts");
2415
+ const buClr = firstChild(pPr, "a:buClr");
2416
+ const colorEl = buClr ? firstChildElement(buClr) : null;
2417
+ const pctVal = buSzPct ? intValue(attr(buSzPct, "val")) : null;
2418
+ const ptVal = buSzPts ? intValue(attr(buSzPts, "val")) : null;
2419
+ return {
2420
+ font: buFont ? attr(buFont, "typeface") ?? null : null,
2421
+ sizePct: pctVal === null ? null : pctVal / 1e3,
2422
+ sizePt: ptVal === null ? null : ptVal / 100,
2423
+ color: colorEl?.localName === "srgbClr" ? attr(colorEl, "val") ?? null : null,
2424
+ schemeColor: colorEl?.localName === "schemeClr" ? attr(colorEl, "val") ?? null : null,
2425
+ resolvedColor: this.themeContext ? resolveColorElement(colorEl, this.themeContext) : null
2426
+ };
2427
+ }
2428
+ /** Resolve a picture bullet's `a:buBlip/a:blip/@r:embed` to an absolute partname. */
2429
+ #bulletImagePartName(buBlip) {
2430
+ const blip = firstChild(buBlip, "a:blip");
2431
+ const relId = blip && attr(blip, "r:embed");
2432
+ if (!relId || !this.relationships) return null;
2433
+ return this.relationships.resolveTarget(relId);
2434
+ }
2652
2435
  /** Points from a spacing child's `a:spcPts/@val` (hundredths of a point), or `null`. */
2653
2436
  #spacingPt(qname) {
2654
2437
  const pPr = firstChild(this.element, "a:pPr");
@@ -2831,7 +2614,7 @@ function setTextBodyText(txBody, value) {
2831
2614
  p = createElement(doc, "a:p");
2832
2615
  txBody.appendChild(p);
2833
2616
  }
2834
- for (const child of [...childElements$2(p)]) if (child.localName === "r" || child.localName === "br" || child.localName === "fld") p.removeChild(child);
2617
+ for (const child of [...childElements(p)]) if (child.localName === "r" || child.localName === "br" || child.localName === "fld") p.removeChild(child);
2835
2618
  const run = createElement(doc, "a:r");
2836
2619
  if (rPrTemplate) run.appendChild(rPrTemplate.cloneNode(true));
2837
2620
  const t = createElement(doc, "a:t");
@@ -2843,119 +2626,6 @@ function setTextBodyText(txBody, value) {
2843
2626
  p.insertBefore(run, endParaRPr);
2844
2627
  }
2845
2628
  //#endregion
2846
- //#region src/read/api/shapes/geometry.ts
2847
- /**
2848
- * Reading geometry out of a shape: transform boxes, rotation and flips, and custom-geometry paths.
2849
- *
2850
- * Every value here comes straight off `a:xfrm` / `a:path` attributes, so these are pure element
2851
- * readers with no theme or relationship context.
2852
- */
2853
- /**
2854
- * One `a:pt` coordinate as a raw path-unit integer. A guide-name reference
2855
- * (the `ST_AdjCoordinate` string form) is not produced by authored freeforms;
2856
- * a non-numeric value degrades to `0` rather than crashing (documented edge).
2857
- */
2858
- function ptAxis(pt, axis) {
2859
- return (pt ? intValue(attr(pt, axis)) : null) ?? 0;
2860
- }
2861
- /** Parse one `<a:path>` into its viewport attrs (with schema defaults) and ordered segments. */
2862
- function readGeometryPath(path) {
2863
- const commands = [];
2864
- for (let node = path.firstChild; node; node = node.nextSibling) {
2865
- if (node.nodeType !== 1) continue;
2866
- const seg = node;
2867
- if (seg.namespaceURI !== OOXML_NS.a) continue;
2868
- const pts = getElements(seg, "a:pt");
2869
- switch (seg.localName) {
2870
- case "moveTo":
2871
- commands.push({
2872
- cmd: "moveTo",
2873
- x: ptAxis(pts[0], "x"),
2874
- y: ptAxis(pts[0], "y")
2875
- });
2876
- break;
2877
- case "lnTo":
2878
- commands.push({
2879
- cmd: "lnTo",
2880
- x: ptAxis(pts[0], "x"),
2881
- y: ptAxis(pts[0], "y")
2882
- });
2883
- break;
2884
- case "cubicBezTo":
2885
- commands.push({
2886
- cmd: "cubicBezTo",
2887
- x1: ptAxis(pts[0], "x"),
2888
- y1: ptAxis(pts[0], "y"),
2889
- x2: ptAxis(pts[1], "x"),
2890
- y2: ptAxis(pts[1], "y"),
2891
- x: ptAxis(pts[2], "x"),
2892
- y: ptAxis(pts[2], "y")
2893
- });
2894
- break;
2895
- case "quadBezTo":
2896
- commands.push({
2897
- cmd: "quadBezTo",
2898
- x1: ptAxis(pts[0], "x"),
2899
- y1: ptAxis(pts[0], "y"),
2900
- x: ptAxis(pts[1], "x"),
2901
- y: ptAxis(pts[1], "y")
2902
- });
2903
- break;
2904
- case "arcTo":
2905
- commands.push({
2906
- cmd: "arcTo",
2907
- wR: intValue(attr(seg, "wR")) ?? 0,
2908
- hR: intValue(attr(seg, "hR")) ?? 0,
2909
- stAng: (intValue(attr(seg, "stAng")) ?? 0) / 6e4,
2910
- swAng: (intValue(attr(seg, "swAng")) ?? 0) / 6e4
2911
- });
2912
- break;
2913
- case "close":
2914
- commands.push({ cmd: "close" });
2915
- break;
2916
- }
2917
- }
2918
- return {
2919
- w: intValue(attr(path, "w")) ?? 0,
2920
- h: intValue(attr(path, "h")) ?? 0,
2921
- fill: attr(path, "fill") ?? "norm",
2922
- stroke: boolValue(attr(path, "stroke")) ?? true,
2923
- commands
2924
- };
2925
- }
2926
- /** Validate and round an EMU geometry value; extents (`cx`/`cy`) must be non-negative. */
2927
- function toEmu(value, attribute, allowNegative) {
2928
- if (!Number.isFinite(value)) throw new InvalidOptionError("coord/non-finite", `${attribute} must be a finite number of EMU, got ${value}`);
2929
- if (!allowNegative && value < 0) throw new InvalidOptionError("coord/negative", `${attribute} must be non-negative, got ${value}`);
2930
- return Math.round(value);
2931
- }
2932
- /** A point + extent pair (`a:off`/`a:ext` or `a:chOff`/`a:chExt`) from a transform, or `null` if either is incomplete. */
2933
- function readBox(xfrm, offName, extName) {
2934
- const off = firstChild(xfrm, offName);
2935
- const ext = firstChild(xfrm, extName);
2936
- const x = off && intValue(attr(off, "x"));
2937
- const y = off && intValue(attr(off, "y"));
2938
- const cx = ext && intValue(attr(ext, "cx"));
2939
- const cy = ext && intValue(attr(ext, "cy"));
2940
- if (x === null || y === null || cx === null || cy === null) return null;
2941
- return {
2942
- x,
2943
- y,
2944
- cx,
2945
- cy
2946
- };
2947
- }
2948
- function rotationDegrees(xfrm) {
2949
- const rot = intValue(attr(xfrm, "rot"));
2950
- return rot === null ? 0 : rot / 6e4;
2951
- }
2952
- function transformFlipH(xfrm) {
2953
- return boolValue(attr(xfrm, "flipH")) === true;
2954
- }
2955
- function transformFlipV(xfrm) {
2956
- return boolValue(attr(xfrm, "flipV")) === true;
2957
- }
2958
- //#endregion
2959
2629
  //#region src/read/api/shapes/base.ts
2960
2630
  /**
2961
2631
  * The abstract base every shape kind extends.
@@ -3254,9 +2924,25 @@ var Shape = class {
3254
2924
  });
3255
2925
  }
3256
2926
  /**
2927
+ * `true` when the shape sets an explicit no-fill (`spPr/a:noFill`) — a
2928
+ * deliberately transparent surface. The fill-side counterpart of
2929
+ * {@link lineNoFill}, and the only accessor that separates it from a shape
2930
+ * carrying no fill child at all (one inheriting through `p:style/a:fillRef`):
2931
+ * every other fill accessor — {@link fillColor}, {@link fillSchemeColor},
2932
+ * {@link resolvedFill}, {@link gradientFill}, {@link patternFill},
2933
+ * {@link pictureFill} — reports `null` for both. The two paint completely
2934
+ * differently, so a consumer that cannot tell them apart paints a transparent
2935
+ * shape in the theme's accent colour.
2936
+ */
2937
+ get fillNoFill() {
2938
+ const props = this.properties();
2939
+ return !!(props && firstChild(props, "a:noFill"));
2940
+ }
2941
+ /**
3257
2942
  * Set an explicit `<a:noFill/>` on the shape — a transparent surface. This is
3258
2943
  * distinct from clearing the fill (`fillColor = null`), which removes the
3259
2944
  * `a:solidFill` and lets the fill inherit from the shape's style/placeholder.
2945
+ * Read it back with {@link fillNoFill}.
3260
2946
  */
3261
2947
  noFill() {
3262
2948
  if (!this.supportsFill) throw new UnsupportedFeatureError("shape/fill-unsupported", `${this.shapeType} shapes do not support a solid fill`);
@@ -3314,6 +3000,32 @@ var Shape = class {
3314
3000
  return !!(ln && firstChild(ln, "a:noFill"));
3315
3001
  }
3316
3002
  /**
3003
+ * The line end cap (`spPr/a:ln/@cap`) as the raw OOXML token — `'flat'`,
3004
+ * `'rnd'` (round) or `'sq'` (square) — or `null` when unset (PowerPoint's
3005
+ * default is `flat`). The write API authors this attribute through
3006
+ * `ShapeLineProps.cap`, so without the accessor a deck this library produced
3007
+ * could not be read back without losing it.
3008
+ *
3009
+ * Not cosmetic on a thick dashed rule: the cap decides whether each dash reads
3010
+ * as a rectangle or a lozenge, and it extends every dash by the stroke width.
3011
+ * SVG's `stroke-linecap` is the exact equivalent (`flat`→`butt`, `rnd`→`round`,
3012
+ * `sq`→`square`).
3013
+ */
3014
+ get lineCap() {
3015
+ const ln = this.#line();
3016
+ return ln ? attr(ln, "cap") ?? null : null;
3017
+ }
3018
+ /**
3019
+ * The line alignment (`spPr/a:ln/@algn`) as the raw OOXML token — `'ctr'`
3020
+ * (centred on the shape's outline) or `'in'` (inset, drawn wholly inside it) —
3021
+ * or `null` when unset. It shifts a thick outline by half its width, so it
3022
+ * changes where the border sits relative to the fill.
3023
+ */
3024
+ get lineAlign() {
3025
+ const ln = this.#line();
3026
+ return ln ? attr(ln, "algn") ?? null : null;
3027
+ }
3028
+ /**
3317
3029
  * Preset geometry name (`spPr/a:prstGeom/@prst`, e.g. `rect`), or `null` for
3318
3030
  * custom geometry or none. Not an auto-shape-only property: PowerPoint gives a
3319
3031
  * picture and a connector a preset geometry too (a `p:pic` is `rect` unless it
@@ -3491,19 +3203,7 @@ var Shape = class {
3491
3203
  */
3492
3204
  get patternFill() {
3493
3205
  const props = this.properties();
3494
- const patt = props && firstChild(props, "a:pattFill");
3495
- if (!patt) return null;
3496
- const ctx = this.slide.themeContext();
3497
- const resolveWrap = (qname) => {
3498
- const wrap = firstChild(patt, qname);
3499
- const colorEl = wrap && firstChildElement(wrap);
3500
- return colorEl ? resolveColorElement(colorEl, ctx) : null;
3501
- };
3502
- return {
3503
- preset: attr(patt, "prst") ?? null,
3504
- foreground: resolveWrap("a:fgClr"),
3505
- background: resolveWrap("a:bgClr")
3506
- };
3206
+ return props ? readPatternFill(props, this.slide.themeContext()) : null;
3507
3207
  }
3508
3208
  /**
3509
3209
  * The shape's picture (image) fill (`spPr/a:blipFill`), or `null` when the fill
@@ -3726,7 +3426,6 @@ var AutoShape = class extends Shape {
3726
3426
  * blip plumbing (content-type → extension, the SVG extension blip, the recolour effects) lives
3727
3427
  * here rather than on the base: no other shape kind carries it.
3728
3428
  */
3729
- const IMAGE_REL_TYPE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image";
3730
3429
  const ASVG_NS = "http://schemas.microsoft.com/office/drawing/2016/SVG/main";
3731
3430
  const PIC_AFTER_BLIPFILL = ["p:spPr", "p:style"];
3732
3431
  const BLIPFILL_AFTER_BLIP = [
@@ -3969,7 +3668,7 @@ var Picture = class extends Shape {
3969
3668
  const opc = this.slide.presentation.opc;
3970
3669
  const mediaPartName = opc.reserveMediaPartName(extension);
3971
3670
  opc.addPart(mediaPartName, contentType, bytes);
3972
- const relId = this.slide.relationships.add(IMAGE_REL_TYPE, relativePartName(this.slide.partName, mediaPartName)).id;
3671
+ const relId = this.slide.relationships.add(IMAGE_REL, relativePartName(this.slide.partName, mediaPartName)).id;
3973
3672
  setAttr(this.#getOrAddBlip(), "r:embed", relId);
3974
3673
  if (options.fit) this.#applyFit(options.fit, bytes);
3975
3674
  this.markDirty();
@@ -4355,7 +4054,7 @@ var ChartSeries = class {
4355
4054
  /** Cached numeric values (`c:val`); non-numeric or missing points are `null`. */
4356
4055
  get values() {
4357
4056
  const val = firstChild(this.ser, "c:val");
4358
- return readPoints(val && findCache(val)).map(toNumberOrNull$1);
4057
+ return readPoints(val && findCache(val)).map(intValue);
4359
4058
  }
4360
4059
  /** Cached category labels for this series (`c:cat`), as written. */
4361
4060
  get categories() {
@@ -4441,12 +4140,7 @@ function readPoints(cache) {
4441
4140
  const idx = intValue(attr(pt, "idx")) ?? 0;
4442
4141
  points[idx] = firstChild(pt, "c:v")?.textContent ?? null;
4443
4142
  }
4444
- return points;
4445
- }
4446
- function toNumberOrNull$1(value) {
4447
- if (value === null || value === "") return null;
4448
- const number = Number(value);
4449
- return Number.isFinite(number) ? number : null;
4143
+ return points;
4450
4144
  }
4451
4145
  //#endregion
4452
4146
  //#region src/read/api/chartex.ts
@@ -4591,7 +4285,7 @@ var ChartExSeries = class {
4591
4285
  get values() {
4592
4286
  const data = this.chart.dataElementById(this.dataId);
4593
4287
  const numDim = data && firstChild(data, "cx:numDim");
4594
- return readLevelPoints(numDim && firstChild(numDim, "cx:lvl")).map(toNumberOrNull);
4288
+ return readLevelPoints(numDim && firstChild(numDim, "cx:lvl")).map(intValue);
4595
4289
  }
4596
4290
  /**
4597
4291
  * Cached category labels (`cx:strDim`'s leaf `cx:lvl` — the first level, which
@@ -4709,10 +4403,391 @@ function floatValue(value) {
4709
4403
  const number = Number(value);
4710
4404
  return Number.isFinite(number) ? number : null;
4711
4405
  }
4712
- function toNumberOrNull(value) {
4713
- if (value === null || value === "") return null;
4714
- const number = Number(value);
4715
- return Number.isFinite(number) ? number : null;
4406
+ //#endregion
4407
+ //#region src/read/api/table-edit.ts
4408
+ /**
4409
+ * Ordered mutation of a table cell's `a:tcPr`, and the cell-scoped schema facts every such
4410
+ * edit needs.
4411
+ *
4412
+ * `CT_TableCellProperties` declares its children as a **sequence**, so an append-only
4413
+ * implementation produces an out-of-order `a:tcPr` — which PowerPoint reports as a corrupt
4414
+ * file rather than as a bad edit. Every insertion therefore goes through
4415
+ * {@link getOrAddChild} / {@link insertInOrder} with the new element's *successors*, which is
4416
+ * what `TCPR_AFTER` tabulates; it and the sequence it derives from live in
4417
+ * `src/ooxml/sequence.ts` alongside the other complexType orderings.
4418
+ *
4419
+ * The attribute values are the other half. A value outside its `ST_` union is equally fatal
4420
+ * and equally silent, so each is checked against the enum before it is written — the same
4421
+ * validate-at-the-boundary rule the write path follows, with the difference that here there
4422
+ * is a caller to throw at rather than a deck to degrade (see `ooxml/check-enum.ts`).
4423
+ */
4424
+ /** `ST_TextAnchoringType` — `a:tcPr/@anchor`. */
4425
+ const ANCHOR_VALUES = TEXT_ANCHORS;
4426
+ /** `ST_TextVerticalType` — `a:tcPr/@vert`. */
4427
+ const VERT_VALUES = TEXT_VERTICAL;
4428
+ /** `ST_TextHorzOverflowType` — `a:tcPr/@horzOverflow`. */
4429
+ const HORZ_OVERFLOW_VALUES = TEXT_HORZ_OVERFLOW;
4430
+ /** The four edges plus the two diagonals, keyed as {@link TableCellEdge} names them. */
4431
+ const EDGE_QNAMES = {
4432
+ left: "a:lnL",
4433
+ right: "a:lnR",
4434
+ top: "a:lnT",
4435
+ bottom: "a:lnB",
4436
+ tlToBr: "a:lnTlToBr",
4437
+ blToTr: "a:lnBlToTr"
4438
+ };
4439
+ /**
4440
+ * Get or create a child of `a:tcPr` at its schema position.
4441
+ * @param {Element} tcPr - the cell's properties element
4442
+ * @param {string} qname - the child to get or create
4443
+ * @returns {Element} the existing or newly-inserted child
4444
+ */
4445
+ function tcPrChild(tcPr, qname) {
4446
+ return getOrAddChild(tcPr, qname, TCPR_AFTER[qname] ?? []);
4447
+ }
4448
+ /**
4449
+ * Insert an already-built subtree into `a:tcPr` at its schema position.
4450
+ * @param {Element} tcPr - the cell's properties element
4451
+ * @param {string} qname - the qname of the node being inserted (decides where it goes)
4452
+ * @param {Element} node - the subtree to insert
4453
+ */
4454
+ function insertTcPrChild(tcPr, qname, node) {
4455
+ insertInOrder(tcPr, node, TCPR_AFTER[qname] ?? []);
4456
+ }
4457
+ /**
4458
+ * Round a measurement to whole EMU, rejecting a value that cannot be written.
4459
+ * `NaN`/`Infinity` would reach the attribute verbatim and make the part invalid, which is
4460
+ * exactly the "silent coercion of invalid input" the project's policy rules out.
4461
+ */
4462
+ function checkFiniteEmu(value, field, code) {
4463
+ if (!Number.isFinite(value)) throw new InvalidOptionError(code, `${field} must be a finite number of EMU, got: ${String(value)}`);
4464
+ return Math.round(value);
4465
+ }
4466
+ //#endregion
4467
+ //#region src/read/api/table-structure.ts
4468
+ /**
4469
+ * Structural edits to an existing `a:tbl`: adding and removing rows and columns, and
4470
+ * merging and unmerging cells.
4471
+ *
4472
+ * The one fact that makes this tractable is that a stored table's grid is already
4473
+ * **rectangular**. Unlike the write path — which receives lopsided row arrays and has to
4474
+ * build the merge grid itself (`gen/slide/objects/table.ts` STEP 3) — every `a:tr` in a
4475
+ * conformant `a:tbl` holds exactly one `a:tc` per grid column, with the covered half of each
4476
+ * span present and flagged. So "the cell at (r, c)" is simply the c-th `a:tc` of the r-th
4477
+ * `a:tr`, and an edit's whole job is keeping that rectangle true.
4478
+ *
4479
+ * Three things have to stay in step, and every function here is written around them:
4480
+ *
4481
+ * 1. `a:tblGrid/a:gridCol` count == each row's `a:tc` count.
4482
+ * 2. A cell with `@gridSpan="n"` is followed by exactly `n-1` cells carrying `@hMerge="1"`,
4483
+ * and a cell with `@rowSpan="n"` is under-hung by `n-1` cells carrying `@vMerge="1"`, in
4484
+ * the same columns.
4485
+ * 3. A covered cell carries no span attributes of its own — its extent is the origin's.
4486
+ *
4487
+ * Breaking any of them produces a table PowerPoint reports as a corrupt file rather than as
4488
+ * a bad edit, which is why the span bookkeeping here is explicit rather than incidental.
4489
+ */
4490
+ /** Every `a:tr` of a table, in document order. */
4491
+ function rowsOf(tbl) {
4492
+ return getElements(tbl, "a:tr");
4493
+ }
4494
+ /** Every `a:tc` of a row, in document (left-to-right) order. */
4495
+ function cellsOf(tr) {
4496
+ return getElements(tr, "a:tc");
4497
+ }
4498
+ /** The table's `a:tblGrid`, or a thrown error — `CT_Table` requires one. */
4499
+ function gridOf(tbl) {
4500
+ const grid = firstChild(tbl, "a:tblGrid");
4501
+ if (!grid) throw new InvalidOptionError("table/column-index-out-of-range", "This table has no a:tblGrid, so its columns cannot be edited");
4502
+ return grid;
4503
+ }
4504
+ /** A cell's `@gridSpan`, defaulting to 1. */
4505
+ function gridSpanOf(tc) {
4506
+ return intValue(attr(tc, "gridSpan")) ?? 1;
4507
+ }
4508
+ /** A cell's `@rowSpan`, defaulting to 1. */
4509
+ function rowSpanOf(tc) {
4510
+ return intValue(attr(tc, "rowSpan")) ?? 1;
4511
+ }
4512
+ /** Whether a cell is the covered half of a horizontal merge. */
4513
+ function isHMerge(tc) {
4514
+ return attr(tc, "hMerge") === "1" || attr(tc, "hMerge") === "true";
4515
+ }
4516
+ /** Whether a cell is the covered half of a vertical merge. */
4517
+ function isVMerge(tc) {
4518
+ return attr(tc, "vMerge") === "1" || attr(tc, "vMerge") === "true";
4519
+ }
4520
+ /**
4521
+ * Set a span attribute, removing it when the span collapses to 1 — the schema default, and
4522
+ * what PowerPoint itself writes for an unmerged cell.
4523
+ */
4524
+ function setSpan(tc, name, value) {
4525
+ if (value <= 1) removeAttr(tc, name);
4526
+ else setAttr(tc, name, String(value));
4527
+ }
4528
+ /**
4529
+ * Walk left from `col` to the origin of the horizontal run containing it.
4530
+ * A run is an origin followed by its `hMerge` continuations, so the origin is the first
4531
+ * cell at or left of `col` that is not one.
4532
+ * @returns the origin's column index, or `col` itself when the cell stands alone
4533
+ */
4534
+ function hRunOrigin(cells, col) {
4535
+ let idx = col;
4536
+ while (idx > 0) {
4537
+ const cell = cells[idx];
4538
+ if (!cell || !isHMerge(cell)) break;
4539
+ idx -= 1;
4540
+ }
4541
+ return idx;
4542
+ }
4543
+ /**
4544
+ * Walk up from `row` to the origin of the vertical run containing `(row, col)`.
4545
+ * @returns the origin's row index, or `row` itself when the cell stands alone
4546
+ */
4547
+ function vRunOrigin(rows, row, col) {
4548
+ let idx = row;
4549
+ while (idx > 0) {
4550
+ const cell = rows[idx]?.[col];
4551
+ if (!cell || !isVMerge(cell)) break;
4552
+ idx -= 1;
4553
+ }
4554
+ return idx;
4555
+ }
4556
+ /** A fresh, empty `a:tc`, shaped the way PowerPoint writes one. */
4557
+ function makeCell(doc) {
4558
+ const tc = createElement(doc, "a:tc");
4559
+ const txBody = createElement(doc, "a:txBody");
4560
+ txBody.appendChild(createElement(doc, "a:bodyPr"));
4561
+ txBody.appendChild(createElement(doc, "a:lstStyle"));
4562
+ txBody.appendChild(createElement(doc, "a:p"));
4563
+ tc.appendChild(txBody);
4564
+ tc.appendChild(createElement(doc, "a:tcPr"));
4565
+ return tc;
4566
+ }
4567
+ /** The grid as a row-major array of `a:tc` elements. */
4568
+ function gridOfCells(tbl) {
4569
+ return rowsOf(tbl).map((tr) => cellsOf(tr));
4570
+ }
4571
+ /** Guard an index against `[0, limit]` (inclusive upper bound, for insertion points). */
4572
+ function checkIndex(index, limit, what, inclusive) {
4573
+ const max = inclusive ? limit : limit - 1;
4574
+ if (!Number.isInteger(index) || index < 0 || index > max) throw new InvalidOptionError(what === "row" ? "table/row-index-out-of-range" : "table/column-index-out-of-range", `${what} index ${String(index)} is out of range (expected 0..${max})`);
4575
+ return index;
4576
+ }
4577
+ /**
4578
+ * Insert a row at `index` (default: append).
4579
+ *
4580
+ * The subtle case is inserting **through** a vertical merge. If the row currently at `index`
4581
+ * holds a `vMerge` continuation in some column, the span it belongs to straddles the
4582
+ * insertion point — so the new row must continue that span rather than interrupt it: the
4583
+ * origin's `@rowSpan` grows by one and the new cell is another continuation. Interrupting it
4584
+ * instead would leave an origin claiming more rows than it has continuations, which is the
4585
+ * corrupt-file case.
4586
+ */
4587
+ function insertRow(tbl, index) {
4588
+ const rows = rowsOf(tbl);
4589
+ const at = index === void 0 ? rows.length : checkIndex(index, rows.length, "row", true);
4590
+ const grid = gridOfCells(tbl);
4591
+ const colCount = getElements(gridOf(tbl), "a:gridCol").length;
4592
+ const doc = ownerDocumentOf(tbl);
4593
+ const tr = createElement(doc, "a:tr");
4594
+ setAttr(tr, "h", "0");
4595
+ for (let col = 0; col < colCount; col++) {
4596
+ const tc = makeCell(doc);
4597
+ const displaced = grid[at]?.[col];
4598
+ if (displaced && isVMerge(displaced)) {
4599
+ const origin = grid[vRunOrigin(grid, at, col)]?.[col];
4600
+ if (origin) setSpan(origin, "rowSpan", rowSpanOf(origin) + 1);
4601
+ setAttr(tc, "vMerge", "1");
4602
+ if (isHMerge(displaced)) setAttr(tc, "hMerge", "1");
4603
+ const span = gridSpanOf(displaced);
4604
+ if (span > 1) setSpan(tc, "gridSpan", span);
4605
+ }
4606
+ tr.appendChild(tc);
4607
+ }
4608
+ const before = rows[at] ?? null;
4609
+ tbl.insertBefore(tr, before);
4610
+ return tr;
4611
+ }
4612
+ /**
4613
+ * Remove the row at `index`.
4614
+ *
4615
+ * Two span cases, and they pull in opposite directions. A cell in this row that *continues*
4616
+ * a span from above shortens that span by one. A cell in this row that *starts* one cannot
4617
+ * simply take its span away — the rows below still hold its continuations — so the first
4618
+ * continuation is promoted to origin and inherits the remaining extent. Its content is gone
4619
+ * with the row, which is inherent to removing a row rather than a choice made here.
4620
+ */
4621
+ function removeRow(tbl, index) {
4622
+ const rows = rowsOf(tbl);
4623
+ const at = checkIndex(index, rows.length, "row", false);
4624
+ const grid = gridOfCells(tbl);
4625
+ const row = grid[at] ?? [];
4626
+ for (let col = 0; col < row.length; col++) {
4627
+ const tc = row[col];
4628
+ if (!tc) continue;
4629
+ if (isVMerge(tc)) {
4630
+ const origin = grid[vRunOrigin(grid, at, col)]?.[col];
4631
+ if (origin) setSpan(origin, "rowSpan", rowSpanOf(origin) - 1);
4632
+ continue;
4633
+ }
4634
+ const span = rowSpanOf(tc);
4635
+ if (span > 1) {
4636
+ const heir = grid[at + 1]?.[col];
4637
+ if (heir) {
4638
+ removeAttr(heir, "vMerge");
4639
+ setSpan(heir, "rowSpan", span - 1);
4640
+ setSpan(heir, "gridSpan", gridSpanOf(tc));
4641
+ }
4642
+ }
4643
+ }
4644
+ const tr = rows[at];
4645
+ if (tr) tbl.removeChild(tr);
4646
+ }
4647
+ /**
4648
+ * Insert a column at `index` (default: append), `widthEmu` wide.
4649
+ *
4650
+ * Mirrors {@link insertRow}'s split case on the other axis: when the cell currently at
4651
+ * `index` is an `hMerge` continuation, the insertion point falls inside a horizontal span,
4652
+ * so the span widens by one and the new cell joins it as another continuation.
4653
+ */
4654
+ function insertColumn(tbl, index, widthEmu = 914400) {
4655
+ const grid = gridOf(tbl);
4656
+ const cols = getElements(grid, "a:gridCol");
4657
+ const at = index === void 0 ? cols.length : checkIndex(index, cols.length, "column", true);
4658
+ const doc = ownerDocumentOf(tbl);
4659
+ const gridCol = createElement(doc, "a:gridCol");
4660
+ setAttr(gridCol, "w", String(Math.round(widthEmu)));
4661
+ grid.insertBefore(gridCol, cols[at] ?? null);
4662
+ for (const tr of rowsOf(tbl)) {
4663
+ const cells = cellsOf(tr);
4664
+ const tc = makeCell(doc);
4665
+ const displaced = cells[at];
4666
+ if (displaced && isHMerge(displaced)) {
4667
+ const origin = cells[hRunOrigin(cells, at)];
4668
+ if (origin) setSpan(origin, "gridSpan", gridSpanOf(origin) + 1);
4669
+ setAttr(tc, "hMerge", "1");
4670
+ if (isVMerge(displaced)) setAttr(tc, "vMerge", "1");
4671
+ const span = rowSpanOf(displaced);
4672
+ if (span > 1) setSpan(tc, "rowSpan", span);
4673
+ }
4674
+ tr.insertBefore(tc, cells[at] ?? firstChild(tr, "a:extLst"));
4675
+ }
4676
+ return gridCol;
4677
+ }
4678
+ /**
4679
+ * Remove the column at `index`.
4680
+ *
4681
+ * When the column falls inside a horizontal span, the span shrinks by one and a
4682
+ * *continuation* is removed rather than the origin — so the merged region narrows but keeps
4683
+ * its content. Only a column whose cell stands alone loses that cell outright.
4684
+ */
4685
+ function removeColumn(tbl, index) {
4686
+ const grid = gridOf(tbl);
4687
+ const cols = getElements(grid, "a:gridCol");
4688
+ const at = checkIndex(index, cols.length, "column", false);
4689
+ const col = cols[at];
4690
+ if (col) grid.removeChild(col);
4691
+ for (const tr of rowsOf(tbl)) {
4692
+ const cells = cellsOf(tr);
4693
+ const originIdx = hRunOrigin(cells, at);
4694
+ const origin = cells[originIdx];
4695
+ if (!origin) continue;
4696
+ const span = gridSpanOf(origin);
4697
+ if (span > 1) {
4698
+ setSpan(origin, "gridSpan", span - 1);
4699
+ const victim = cells[originIdx + span - 1];
4700
+ if (victim) tr.removeChild(victim);
4701
+ continue;
4702
+ }
4703
+ const tc = cells[at];
4704
+ if (tc) tr.removeChild(tc);
4705
+ }
4706
+ }
4707
+ /**
4708
+ * Merge the rectangle `(row1, col1)`–`(row2, col2)` into one cell.
4709
+ *
4710
+ * The top-left cell becomes the origin and keeps its content; every other cell in the
4711
+ * rectangle becomes a covered cell — flagged, stripped of its own span attributes, and
4712
+ * emptied, since a covered cell is never rendered.
4713
+ *
4714
+ * A rectangle whose boundary **cuts through** an existing merge is rejected rather than
4715
+ * silently widened. Widening would be the friendlier-looking choice and the wrong one: the
4716
+ * caller asked for a specific region, and quietly returning a different one is how a layout
4717
+ * ends up subtly wrong with nothing to point at.
4718
+ */
4719
+ function mergeCells(tbl, row1, col1, row2, col2) {
4720
+ const grid = gridOfCells(tbl);
4721
+ const rowCount = grid.length;
4722
+ const colCount = getElements(gridOf(tbl), "a:gridCol").length;
4723
+ const r1 = Math.min(row1, row2);
4724
+ const r2 = Math.max(row1, row2);
4725
+ const c1 = Math.min(col1, col2);
4726
+ const c2 = Math.max(col1, col2);
4727
+ checkIndex(r1, rowCount, "row", false);
4728
+ checkIndex(r2, rowCount, "row", false);
4729
+ checkIndex(c1, colCount, "column", false);
4730
+ checkIndex(c2, colCount, "column", false);
4731
+ if (r1 === r2 && c1 === c2) throw new InvalidOptionError("table/merge-range-invalid", "A merge needs at least two cells; the given range covers one");
4732
+ for (let r = r1; r <= r2; r++) for (let c = c1; c <= c2; c++) {
4733
+ if (!grid[r]?.[c]) continue;
4734
+ const originRow = vRunOrigin(grid, r, c);
4735
+ const originCol = hRunOrigin(grid[r] ?? [], c);
4736
+ const origin = grid[originRow]?.[originCol];
4737
+ if (!origin) continue;
4738
+ if (originRow < r1 || originCol < c1) throw new InvalidOptionError("table/merge-range-invalid", `The range (${r1},${c1})-(${r2},${c2}) starts inside an existing merged cell at (${originRow},${originCol}); unmerge it first`);
4739
+ if (originRow + rowSpanOf(origin) - 1 > r2 || originCol + gridSpanOf(origin) - 1 > c2) throw new InvalidOptionError("table/merge-range-invalid", `The range (${r1},${c1})-(${r2},${c2}) cuts through an existing merged cell at (${originRow},${originCol}); unmerge it first`);
4740
+ }
4741
+ const origin = grid[r1]?.[c1];
4742
+ if (!origin) return;
4743
+ setSpan(origin, "gridSpan", c2 - c1 + 1);
4744
+ setSpan(origin, "rowSpan", r2 - r1 + 1);
4745
+ removeAttr(origin, "hMerge");
4746
+ removeAttr(origin, "vMerge");
4747
+ for (let r = r1; r <= r2; r++) for (let c = c1; c <= c2; c++) {
4748
+ if (r === r1 && c === c1) continue;
4749
+ const tc = grid[r]?.[c];
4750
+ if (!tc) continue;
4751
+ removeAttr(tc, "gridSpan");
4752
+ removeAttr(tc, "rowSpan");
4753
+ if (c > c1) setAttr(tc, "hMerge", "1");
4754
+ else removeAttr(tc, "hMerge");
4755
+ if (r > r1) setAttr(tc, "vMerge", "1");
4756
+ else removeAttr(tc, "vMerge");
4757
+ emptyCellText(tc);
4758
+ }
4759
+ }
4760
+ /**
4761
+ * Split the merged cell whose origin is `(row, col)` back into individual cells.
4762
+ * The origin keeps its content; the cells it covered come back empty, which is what they
4763
+ * already were.
4764
+ */
4765
+ function unmergeCell(tbl, row, col) {
4766
+ const grid = gridOfCells(tbl);
4767
+ checkIndex(row, grid.length, "row", false);
4768
+ checkIndex(col, getElements(gridOf(tbl), "a:gridCol").length, "column", false);
4769
+ const origin = grid[row]?.[col];
4770
+ if (!origin) return;
4771
+ if (isHMerge(origin) || isVMerge(origin)) throw new InvalidOptionError("table/merge-range-invalid", `The cell at (${row},${col}) is a covered cell, not a merge origin; unmerge the origin at (${vRunOrigin(grid, row, col)},${hRunOrigin(grid[row] ?? [], col)}) instead`);
4772
+ const rowSpan = rowSpanOf(origin);
4773
+ const gridSpan = gridSpanOf(origin);
4774
+ if (rowSpan === 1 && gridSpan === 1) return;
4775
+ removeAttr(origin, "gridSpan");
4776
+ removeAttr(origin, "rowSpan");
4777
+ for (let r = row; r < row + rowSpan; r++) for (let c = col; c < col + gridSpan; c++) {
4778
+ if (r === row && c === col) continue;
4779
+ const tc = grid[r]?.[c];
4780
+ if (!tc) continue;
4781
+ removeAttr(tc, "hMerge");
4782
+ removeAttr(tc, "vMerge");
4783
+ }
4784
+ }
4785
+ /** Reduce a cell's text body to a single empty paragraph. */
4786
+ function emptyCellText(tc) {
4787
+ const txBody = firstChild(tc, "a:txBody");
4788
+ if (!txBody) return;
4789
+ for (const p of getElements(txBody, "a:p")) txBody.removeChild(p);
4790
+ txBody.appendChild(createElement(ownerDocumentOf(txBody), "a:p"));
4716
4791
  }
4717
4792
  //#endregion
4718
4793
  //#region src/read/api/table-style-resolve.ts
@@ -4893,6 +4968,63 @@ var Table = class {
4893
4968
  const id = (tblPr && firstChild(tblPr, "a:tableStyleId"))?.textContent?.trim();
4894
4969
  return id ? id : null;
4895
4970
  }
4971
+ /**
4972
+ * The table's own background (`a:tblPr` fill) resolved against the slide's theme colour
4973
+ * context to a literal hex, or `null` when the table carries no solid background.
4974
+ *
4975
+ * This is the table-level counterpart of {@link TableCell.resolvedFill}, and it is a
4976
+ * genuinely different thing from a cell fill: a `a:tblPr` fill sits *behind* the grid, so
4977
+ * a cell with no fill of its own shows it through. Reports `null` for a non-solid choice
4978
+ * (`a:blipFill`/`a:gradFill`/`a:pattFill`/`a:noFill`) — read {@link pictureFill} for an
4979
+ * image background — and `null` with no theme context.
4980
+ */
4981
+ get resolvedFill() {
4982
+ if (!this.themeColors) return null;
4983
+ const tblPr = firstChild(this.tbl, "a:tblPr");
4984
+ if (!tblPr || !FILL_CHOICES.some((q) => firstChild(tblPr, q))) return null;
4985
+ return resolveSolidFillColor(tblPr, this.themeColors);
4986
+ }
4987
+ /**
4988
+ * The table's picture background (`a:tblPr/a:blipFill`), or `null` when the table is not
4989
+ * image-backed. The table-level twin of {@link TableCell.pictureFill}, and needed for the
4990
+ * same reason: {@link resolvedFill} decodes only solid colours, so without this an
4991
+ * image-backed table is indistinguishable from an unfilled one.
4992
+ */
4993
+ get pictureFill() {
4994
+ const tblPr = firstChild(this.tbl, "a:tblPr");
4995
+ return tblPr ? readPictureFill(tblPr, this.rels ?? null) : null;
4996
+ }
4997
+ /**
4998
+ * The table's gradient background (`a:tblPr/a:gradFill`), or `null` when it is not
4999
+ * gradient-filled. Needed for the same reason {@link pictureFill} is:
5000
+ * {@link resolvedFill} decodes only solid colours.
5001
+ */
5002
+ get gradientFill() {
5003
+ if (!this.themeColors) return null;
5004
+ const tblPr = firstChild(this.tbl, "a:tblPr");
5005
+ return tblPr ? readGradientFill(tblPr, this.themeColors) : null;
5006
+ }
5007
+ /**
5008
+ * The table's pattern (hatch) background (`a:tblPr/a:pattFill`), or `null` when it is not
5009
+ * pattern-filled.
5010
+ */
5011
+ get patternFill() {
5012
+ if (!this.themeColors) return null;
5013
+ const tblPr = firstChild(this.tbl, "a:tblPr");
5014
+ return tblPr ? readPatternFill(tblPr, this.themeColors) : null;
5015
+ }
5016
+ /**
5017
+ * The raw `schemeClr` token of the table's own background
5018
+ * (`a:tblPr/a:solidFill/a:schemeClr/@val`), or `null` for an absent or `srgbClr` fill.
5019
+ * {@link resolvedFill} is the literal it resolves to; this is the unresolved reference,
5020
+ * which is what a replica should carry so the copy still tracks its theme.
5021
+ */
5022
+ get fillSchemeColor() {
5023
+ const tblPr = firstChild(this.tbl, "a:tblPr");
5024
+ const fill = tblPr && firstChild(tblPr, "a:solidFill");
5025
+ const scheme = fill && firstChild(fill, "a:schemeClr");
5026
+ return scheme ? attr(scheme, "val") : null;
5027
+ }
4896
5028
  /** Whether the first row is styled as a header (`a:tblPr/@firstRow`). */
4897
5029
  get firstRowHeader() {
4898
5030
  return this.#tblPrFlag("firstRow");
@@ -4909,6 +5041,83 @@ var Table = class {
4909
5041
  cell(rowIndex, columnIndex) {
4910
5042
  return this.rows[rowIndex]?.cells[columnIndex] ?? null;
4911
5043
  }
5044
+ /**
5045
+ * Insert a row, defaulting to the bottom of the table. Returns the new {@link TableRow}.
5046
+ *
5047
+ * The new row is auto-height (`a:tr/@h="0"`) and its cells are empty. Inserting *through*
5048
+ * a vertical merge extends that merge rather than interrupting it: the span's origin
5049
+ * grows by a row and the new cell joins as a continuation, because an origin claiming
5050
+ * more rows than it has continuations is a table PowerPoint reports as corrupt.
5051
+ * @param {number} [index] - where to insert, `0`..`rowCount`; appended when omitted
5052
+ * @throws {InvalidOptionError} when `index` is outside that range
5053
+ */
5054
+ addRow(index) {
5055
+ const tr = insertRow(this.tbl, index);
5056
+ this.part.markDirty();
5057
+ return new TableRow(tr, this.part, this.themeColors, this.#styleContext(), rowsOf(this.tbl).indexOf(tr), this.rels);
5058
+ }
5059
+ /**
5060
+ * Remove the row at `index`, with its content.
5061
+ *
5062
+ * A cell in the row that *continues* a vertical merge shortens that merge. A cell that
5063
+ * *starts* one hands the region to its first continuation, which becomes the new origin —
5064
+ * the merged region survives one row shorter, and only the removed row's own text is lost.
5065
+ * @throws {InvalidOptionError} when `index` is out of range
5066
+ */
5067
+ removeRow(index) {
5068
+ removeRow(this.tbl, index);
5069
+ this.part.markDirty();
5070
+ }
5071
+ /**
5072
+ * Insert a column, defaulting to the right-hand end. Updates `a:tblGrid` **and** every
5073
+ * row, which is the pair that has to stay in step.
5074
+ *
5075
+ * The mirror of {@link addRow}'s merge case: inserting inside a horizontal merge widens
5076
+ * it instead of splitting it.
5077
+ * @param {number} [index] - where to insert, `0`..`columnCount`; appended when omitted
5078
+ * @param {number} [widthEmu] - the new column's width; defaults to 1 inch
5079
+ * @throws {InvalidOptionError} when `index` is outside that range
5080
+ */
5081
+ addColumn(index, widthEmu) {
5082
+ insertColumn(this.tbl, index, widthEmu);
5083
+ this.part.markDirty();
5084
+ }
5085
+ /**
5086
+ * Remove the column at `index`, from `a:tblGrid` and from every row.
5087
+ *
5088
+ * Inside a horizontal merge the region narrows by one and keeps its content: a covered
5089
+ * cell is dropped rather than the origin. Elsewhere the column's cells go with it.
5090
+ * @throws {InvalidOptionError} when `index` is out of range
5091
+ */
5092
+ removeColumn(index) {
5093
+ removeColumn(this.tbl, index);
5094
+ this.part.markDirty();
5095
+ }
5096
+ /**
5097
+ * Merge the rectangle between two cell positions into one cell. The top-left cell keeps
5098
+ * its content; the rest become covered cells and are emptied, since a covered cell is
5099
+ * never rendered.
5100
+ *
5101
+ * A rectangle that cuts through an existing merge is **rejected**, not silently widened
5102
+ * to fit — the caller asked for a specific region, and quietly producing a different one
5103
+ * is how a layout ends up wrong with nothing to point at. Unmerge first.
5104
+ * @throws {InvalidOptionError} when an index is out of range, the range covers one cell,
5105
+ * or it partially overlaps an existing merge
5106
+ */
5107
+ mergeCells(row1, col1, row2, col2) {
5108
+ mergeCells(this.tbl, row1, col1, row2, col2);
5109
+ this.part.markDirty();
5110
+ }
5111
+ /**
5112
+ * Split the merged cell whose **origin** is `(row, col)` back into individual cells.
5113
+ * A no-op on a cell that is not merged; addressing a covered cell instead of the origin
5114
+ * throws, and the message names the origin to use.
5115
+ * @throws {InvalidOptionError} when an index is out of range or the cell is a covered cell
5116
+ */
5117
+ unmergeCell(row, col) {
5118
+ unmergeCell(this.tbl, row, col);
5119
+ this.part.markDirty();
5120
+ }
4912
5121
  /** Escape hatch: the underlying `a:tbl` element. After mutating it call {@link markDirty}, or `save()` writes the original bytes. */
4913
5122
  get element_() {
4914
5123
  return this.tbl;
@@ -4999,6 +5208,183 @@ var TableCell = class {
4999
5208
  return firstChild(this.tc, "a:tcPr");
5000
5209
  }
5001
5210
  /**
5211
+ * The cell's `a:tcPr`, creating it when absent.
5212
+ *
5213
+ * `CT_TableCell` sequences `a:txBody` then `a:tcPr` then `a:extLst`, so a newly created
5214
+ * one is inserted before `a:extLst` — appending it blindly would put it after and make
5215
+ * the part invalid. Every setter below goes through here for that reason.
5216
+ */
5217
+ #getOrAddTcPr() {
5218
+ return getOrAddChild(this.tc, "a:tcPr", ["a:extLst"]);
5219
+ }
5220
+ /**
5221
+ * Set (or clear) the cell's vertical text anchor (`a:tcPr/@anchor`).
5222
+ * `null` removes the attribute, leaving PowerPoint's default of top.
5223
+ * @throws {InvalidOptionError} when the value is outside `ST_TextAnchoringType`
5224
+ */
5225
+ setAnchor(value) {
5226
+ this.#setTcPrAttr("anchor", value, ANCHOR_VALUES, "table/invalid-cell-anchor");
5227
+ }
5228
+ /**
5229
+ * Set (or clear) the cell's text direction (`a:tcPr/@vert`), e.g. `'vert270'`.
5230
+ * `null` removes the attribute, leaving the default horizontal text.
5231
+ * @throws {InvalidOptionError} when the value is outside `ST_TextVerticalType`
5232
+ */
5233
+ setVerticalText(value) {
5234
+ this.#setTcPrAttr("vert", value, VERT_VALUES, "table/invalid-cell-vert");
5235
+ }
5236
+ /**
5237
+ * Set (or clear) `a:tcPr/@horzOverflow` — what a single glyph too wide for the cell does.
5238
+ * `null` removes the attribute; so does `'clip'` in effect, since it is the schema
5239
+ * default, but the attribute is still written as asked.
5240
+ * @throws {InvalidOptionError} when the value is outside `ST_TextHorzOverflowType`
5241
+ */
5242
+ setHorzOverflow(value) {
5243
+ this.#setTcPrAttr("horzOverflow", value, HORZ_OVERFLOW_VALUES, "table/invalid-cell-overflow");
5244
+ }
5245
+ /**
5246
+ * Centre the cell's text block horizontally, or stop doing so (`a:tcPr/@anchorCtr`).
5247
+ * `false` removes the attribute rather than writing `"0"`, since `false` is the schema
5248
+ * default and the two are indistinguishable to a renderer.
5249
+ */
5250
+ setAnchorCtr(value) {
5251
+ const tcPr = this.#getOrAddTcPr();
5252
+ if (value) setAttr(tcPr, "anchorCtr", "1");
5253
+ else removeAttr(tcPr, "anchorCtr");
5254
+ this.part.markDirty();
5255
+ }
5256
+ /**
5257
+ * Set the cell's text insets in EMU (`a:tcPr/@marL`/`@marR`/`@marT`/`@marB`).
5258
+ *
5259
+ * Partial: only the sides named are touched, so `{ left: 0 }` flushes the text left and
5260
+ * leaves the other three alone. A side given as `null` has its attribute removed, which
5261
+ * returns it to the schema default (91440 EMU left/right, 45720 top/bottom) — not to
5262
+ * zero. Pass `{}` to change nothing.
5263
+ * @throws {InvalidOptionError} when a value is not a finite number
5264
+ */
5265
+ setMarginsEmu(margins) {
5266
+ const tcPr = this.#getOrAddTcPr();
5267
+ const sides = [
5268
+ ["marL", margins.left],
5269
+ ["marR", margins.right],
5270
+ ["marT", margins.top],
5271
+ ["marB", margins.bottom]
5272
+ ];
5273
+ for (const [name, value] of sides) {
5274
+ if (value === void 0) continue;
5275
+ if (value === null) removeAttr(tcPr, name);
5276
+ else setAttr(tcPr, name, String(checkFiniteEmu(value, name, "table/invalid-cell-margin")));
5277
+ }
5278
+ this.part.markDirty();
5279
+ }
5280
+ /**
5281
+ * Set (or clear) one of the cell's six borders — the four edges and the two diagonals.
5282
+ *
5283
+ * `null` removes the element entirely, which is "inherit", and is a different thing from
5284
+ * `{ noFill: true }`, which writes an explicit no-border and suppresses whatever would
5285
+ * otherwise be inherited. The element is inserted at its schema position rather than
5286
+ * appended: `CT_TableCellProperties` is a sequence, and an out-of-order `a:tcPr` is
5287
+ * reported by PowerPoint as a corrupt file rather than as a bad edit.
5288
+ *
5289
+ * Colour is either `color` (6-hex, `#` optional) or `schemeColor` (a theme token); giving
5290
+ * both prefers the token, matching how the read side reports them.
5291
+ * @throws {InvalidOptionError} when the width or colour cannot be written
5292
+ */
5293
+ setBorder(edge, border) {
5294
+ const qname = EDGE_QNAMES[edge];
5295
+ if (!qname) throw new InvalidOptionError("table/invalid-cell-border", `Unknown table cell edge: ${JSON.stringify(edge)}. Expected one of: ${Object.keys(EDGE_QNAMES).join(", ")}.`);
5296
+ if (border === null) {
5297
+ const tcPr = this.#tcPr();
5298
+ if (!tcPr || !firstChild(tcPr, qname)) return;
5299
+ removeChildrenByQName(tcPr, [qname]);
5300
+ this.part.markDirty();
5301
+ return;
5302
+ }
5303
+ const tcPr = this.#getOrAddTcPr();
5304
+ removeChildrenByQName(tcPr, [qname]);
5305
+ const doc = ownerDocumentOf(tcPr);
5306
+ const ln = createElement(doc, qname);
5307
+ if (border.widthPt !== void 0 && border.widthPt !== null) setAttr(ln, "w", String(checkFiniteEmu(border.widthPt * 12700, "widthPt", "table/invalid-cell-border")));
5308
+ if (border.noFill) ln.appendChild(createElement(doc, "a:noFill"));
5309
+ else if (border.schemeColor || border.color) {
5310
+ const fill = createElement(doc, "a:solidFill");
5311
+ const scheme = border.schemeColor;
5312
+ const clr = createElement(doc, scheme ? "a:schemeClr" : "a:srgbClr");
5313
+ setAttr(clr, "val", scheme ? scheme : normalizeHex(border.color));
5314
+ fill.appendChild(clr);
5315
+ ln.appendChild(fill);
5316
+ }
5317
+ if (border.dash) {
5318
+ const dash = createElement(doc, "a:prstDash");
5319
+ setAttr(dash, "val", border.dash);
5320
+ ln.appendChild(dash);
5321
+ }
5322
+ insertTcPrChild(tcPr, qname, ln);
5323
+ this.part.markDirty();
5324
+ }
5325
+ /**
5326
+ * Replace the cell's fill with a solid colour (`a:tcPr/a:solidFill`), or clear it.
5327
+ *
5328
+ * `null` removes the `a:solidFill` and lets the cell inherit from the table style again.
5329
+ * That is not the same as {@link noFill}, which writes an explicit `a:noFill` and so
5330
+ * suppresses the inherited shading. Any competing fill choice (`a:gradFill`,
5331
+ * `a:blipFill`, …) is dropped first — `EG_FillProperties` admits one.
5332
+ * @throws {InvalidOptionError} when `color` is not a 6-digit hex string
5333
+ */
5334
+ setFillColor(color) {
5335
+ this.#setFill(color === null ? null : {
5336
+ qname: "a:srgbClr",
5337
+ val: normalizeHex(color)
5338
+ });
5339
+ }
5340
+ /**
5341
+ * Replace the cell's fill with a theme colour token (`a:solidFill/a:schemeClr/@val`), or
5342
+ * clear it. Preferred over {@link setFillColor} when the deck's theme should keep
5343
+ * driving the colour.
5344
+ */
5345
+ setFillSchemeColor(token) {
5346
+ this.#setFill(token === null ? null : {
5347
+ qname: "a:schemeClr",
5348
+ val: token
5349
+ });
5350
+ }
5351
+ /**
5352
+ * Write an explicit `<a:noFill/>` on the cell — a transparent cell that shows the table
5353
+ * background (or the slide) through. Distinct from `setFillColor(null)`, which removes
5354
+ * the fill and lets the table style's shading apply again.
5355
+ */
5356
+ noFill() {
5357
+ const tcPr = this.#getOrAddTcPr();
5358
+ removeChildrenByQName(tcPr, FILL_CHOICES);
5359
+ tcPrChild(tcPr, "a:noFill");
5360
+ this.part.markDirty();
5361
+ }
5362
+ /** Set or clear one `a:tcPr` attribute, vetting it against its schema enum first. */
5363
+ #setTcPrAttr(name, value, valid, code) {
5364
+ if (value === null) {
5365
+ const tcPr = this.#tcPr();
5366
+ if (!tcPr) return;
5367
+ removeAttr(tcPr, name);
5368
+ this.part.markDirty();
5369
+ return;
5370
+ }
5371
+ const checked = checkEnumOrThrow(value, valid, name, code);
5372
+ setAttr(this.#getOrAddTcPr(), name, checked);
5373
+ this.part.markDirty();
5374
+ }
5375
+ /** Replace (or remove) the cell's solid fill, keeping `EG_FillProperties` single-valued. */
5376
+ #setFill(color) {
5377
+ if (color === null) {
5378
+ const tcPr = this.#tcPr();
5379
+ if (!tcPr || !firstChild(tcPr, "a:solidFill")) return;
5380
+ removeChildrenByQName(tcPr, ["a:solidFill"]);
5381
+ this.part.markDirty();
5382
+ return;
5383
+ }
5384
+ setSolidFill(this.#getOrAddTcPr(), TCPR_AFTER["a:solidFill"] ?? [], color);
5385
+ this.part.markDirty();
5386
+ }
5387
+ /**
5002
5388
  * The cell's solid fill resolved against the table's theme colour context to a
5003
5389
  * literal hex — the table-cell counterpart of
5004
5390
  * {@link import('./shapes.js').AutoShape.resolvedFill}. The cell's own
@@ -5020,11 +5406,25 @@ var TableCell = class {
5020
5406
  */
5021
5407
  get resolvedFill() {
5022
5408
  if (!this.themeColors) return null;
5023
- const tcPr = this.#tcPr();
5024
- if (tcPr && FILL_CHOICES.some((q) => firstChild(tcPr, q))) return resolveSolidFillColor(tcPr, this.themeColors);
5409
+ if (this.hasOwnFill) return resolveSolidFillColor(this.#tcPr(), this.themeColors);
5025
5410
  return this.style ? this.#styleFill() : null;
5026
5411
  }
5027
5412
  /**
5413
+ * Whether the cell carries a fill of its **own** (`a:tcPr` holds an `EG_FillProperties`
5414
+ * child), as opposed to inheriting one from the table style's header/banding rules.
5415
+ *
5416
+ * This is the flag that disambiguates {@link resolvedFill}, which deliberately reports
5417
+ * either — it answers "what colour does this cell render as", and both sources are valid
5418
+ * answers to that. Anything that has to reproduce the cell rather than describe it needs
5419
+ * to know which: baking an inherited banding colour into a copy makes the copy look right
5420
+ * until someone changes its table style, and then nothing moves. `false` for a cell whose
5421
+ * `a:tcPr` is empty or absent, whatever the style graph would render it as.
5422
+ */
5423
+ get hasOwnFill() {
5424
+ const tcPr = this.#tcPr();
5425
+ return !!tcPr && FILL_CHOICES.some((q) => firstChild(tcPr, q));
5426
+ }
5427
+ /**
5028
5428
  * The cell's picture (image) fill (`a:tcPr/a:blipFill`), or `null` when the cell
5029
5429
  * is not image-filled. The cell counterpart of
5030
5430
  * {@link import('./shapes.js').AutoShape.pictureFill}: {@link resolvedFill}
@@ -5045,6 +5445,24 @@ var TableCell = class {
5045
5445
  return resolveTableCellStyleFill(s.style.element_, s.flags, this.rowIndex, this.colIndex, s.rowCount, s.colCount, s.ctx);
5046
5446
  }
5047
5447
  /**
5448
+ * The cell's gradient fill (`a:tcPr/a:gradFill`), or `null` when the cell is not
5449
+ * gradient-filled. The cell twin of {@link Table.gradientFill}, and needed for the same
5450
+ * reason {@link pictureFill} is: {@link resolvedFill} reports `null` for every non-solid
5451
+ * choice, so without this a gradient cell is indistinguishable from an unfilled one.
5452
+ */
5453
+ get gradientFill() {
5454
+ const tcPr = this.#tcPr();
5455
+ return tcPr && this.themeColors ? readGradientFill(tcPr, this.themeColors) : null;
5456
+ }
5457
+ /**
5458
+ * The cell's pattern (hatch) fill (`a:tcPr/a:pattFill`), or `null` when the cell is not
5459
+ * pattern-filled.
5460
+ */
5461
+ get patternFill() {
5462
+ const tcPr = this.#tcPr();
5463
+ return tcPr && this.themeColors ? readPatternFill(tcPr, this.themeColors) : null;
5464
+ }
5465
+ /**
5048
5466
  * The raw `schemeClr` token of the cell's solid fill (`a:tcPr/a:solidFill/a:schemeClr/@val`),
5049
5467
  * e.g. `accent1`/`bg1`, or `null` when the fill is absent or an explicit `srgbClr`.
5050
5468
  * The resolved literal is {@link resolvedFill}; this is the unresolved reference.
@@ -5111,6 +5529,88 @@ var TableCell = class {
5111
5529
  return (tcPr && attr(tcPr, "anchor")) ?? null;
5112
5530
  }
5113
5531
  /**
5532
+ * Whether the cell's whole text **block** is centred horizontally within it
5533
+ * (`a:tcPr/@anchorCtr`). `false` when unset — that is the schema default, and it is
5534
+ * what PowerPoint writes nothing for.
5535
+ *
5536
+ * Distinct from a paragraph's `align`, which decides where each *line* sits inside the
5537
+ * text block; this decides where the block sits inside the cell.
5538
+ */
5539
+ get anchorCtr() {
5540
+ const tcPr = this.#tcPr();
5541
+ return tcPr ? attr(tcPr, "anchorCtr") === "1" || attr(tcPr, "anchorCtr") === "true" : false;
5542
+ }
5543
+ /**
5544
+ * The cell's 3-D bevel (`a:tcPr/a:cell3D`), or `null` when it has none.
5545
+ *
5546
+ * `CT_Cell3D` requires an `a:bevel`, so a present `cell3D` always reports a bevel;
5547
+ * `lightRig` is `null` when the cell leaves the scene lighting to the renderer. Sizes are
5548
+ * reported in **points** (the attributes are EMU), matching how the write option takes them.
5549
+ */
5550
+ get cell3D() {
5551
+ const tcPr = this.#tcPr();
5552
+ const cell3D = tcPr && firstChild(tcPr, "a:cell3D");
5553
+ if (!cell3D) return null;
5554
+ const bevel = firstChild(cell3D, "a:bevel");
5555
+ const rig = firstChild(cell3D, "a:lightRig");
5556
+ const pts = (value) => {
5557
+ const emu = intValue(value);
5558
+ return emu === null ? null : emu / 12700;
5559
+ };
5560
+ return {
5561
+ material: attr(cell3D, "prstMaterial") ?? null,
5562
+ bevel: {
5563
+ preset: bevel ? attr(bevel, "prst") ?? null : null,
5564
+ widthPt: bevel ? pts(attr(bevel, "w")) : null,
5565
+ heightPt: bevel ? pts(attr(bevel, "h")) : null
5566
+ },
5567
+ lightRig: rig ? {
5568
+ rig: attr(rig, "rig") ?? null,
5569
+ dir: attr(rig, "dir") ?? null
5570
+ } : null
5571
+ };
5572
+ }
5573
+ /**
5574
+ * The cell's unique identifier (`a:tc/@id`), or `null` when it has none.
5575
+ *
5576
+ * This is what {@link headerIds} references: a data cell names the header cells that
5577
+ * govern it by their `@id`, which is how a complex table tells a screen reader what a
5578
+ * value means. Unrelated to the shape-level `p:cNvPr/@id` — it is an `xs:string` scoped
5579
+ * to the table, not a slide-wide numeric id.
5580
+ *
5581
+ * **PowerPoint does not write this and strips it on save**, so it will read `null` on
5582
+ * any deck PowerPoint has saved, however it was produced. It is surfaced for decks from
5583
+ * other producers. There is deliberately no write-API counterpart — see
5584
+ * `test/read/fixtures/authoring/probe-table-cell-a11y-and-3d.ps1` for the measurement.
5585
+ */
5586
+ get id() {
5587
+ return attr(this.tc, "id") ?? null;
5588
+ }
5589
+ /**
5590
+ * The `@id`s of the header cells that govern this cell
5591
+ * (`a:tcPr/a:headers/a:header/@val`), in document order. Empty when the cell declares no
5592
+ * header association — which is the common case, since only a complex table needs one.
5593
+ *
5594
+ * Carries the same caveat as {@link id}: PowerPoint strips `a:headers` on save, so this
5595
+ * is empty on any PowerPoint-saved deck. `Table.firstRowHeader` (`a:tblPr/@firstRow`) is
5596
+ * the header marker PowerPoint does keep.
5597
+ */
5598
+ get headerIds() {
5599
+ const tcPr = this.#tcPr();
5600
+ const headers = tcPr && firstChild(tcPr, "a:headers");
5601
+ if (!headers) return [];
5602
+ return getElements(headers, "a:header").map((header) => attr(header, "val")).filter((val) => val !== null && val !== "");
5603
+ }
5604
+ /**
5605
+ * How the cell treats a glyph too wide for its text width (`a:tcPr/@horzOverflow`):
5606
+ * `'clip'` cuts it at the cell edge, `'overflow'` lets it draw past. `null` when unset
5607
+ * (PowerPoint clips). Not a wrap flag — cell text always wraps to the column width.
5608
+ */
5609
+ get horzOverflow() {
5610
+ const tcPr = this.#tcPr();
5611
+ return (tcPr && attr(tcPr, "horzOverflow")) ?? null;
5612
+ }
5613
+ /**
5114
5614
  * The cell's text insets in EMU (`a:tcPr/@marL`/`@marR`/`@marT`/`@marB`), or
5115
5615
  * `null` when the cell sets none. Each side is `null` when only some are set.
5116
5616
  */
@@ -5280,6 +5780,30 @@ var GroupShape = class extends Shape {
5280
5780
  "p:graphicFrame"
5281
5781
  ]);
5282
5782
  }
5783
+ /**
5784
+ * The group's own child coordinate space (`p:grpSpPr/a:xfrm/a:chOff` and
5785
+ * `a:chExt`) in EMU, or `null` when the group has no transform or either
5786
+ * element is incomplete.
5787
+ *
5788
+ * Most consumers should not need this: {@link Shape.absoluteFrame} already
5789
+ * composes the whole ancestor chain — child space, group flips and rotations —
5790
+ * and doing that arithmetic per consumer is how a shape in a nested group ends
5791
+ * up subtly displaced. It is here for a *replica* consumer rather than a
5792
+ * *paint* one: rebuilding this group and its children as OOXML needs the
5793
+ * source child space to reproduce the scaling, and without it only groups whose
5794
+ * child space is the identity can be rebuilt.
5795
+ */
5796
+ get childFrame() {
5797
+ const xfrm = this.xfrm();
5798
+ const box = xfrm && readBox(xfrm, "a:chOff", "a:chExt");
5799
+ if (!box) return null;
5800
+ return {
5801
+ offsetX: box.x,
5802
+ offsetY: box.y,
5803
+ extentX: box.cx,
5804
+ extentY: box.cy
5805
+ };
5806
+ }
5283
5807
  /** The shapes nested directly inside this group, in document order. */
5284
5808
  get shapes() {
5285
5809
  return buildShapes(this.element, this.slide);
@@ -5360,6 +5884,6 @@ function buildShapes(parent, slide) {
5360
5884
  return shapes;
5361
5885
  }
5362
5886
  //#endregion
5363
- export { escapeXmlAttribute as $, resolveMasterColorContext as A, restyleSlide as B, Paragraph as C, readGradientFill as D, readPictureFill as E, flattenShape as F, relsPartNameFor as G, partNameExtension as H, flattenSlide as I, OOXML_NS as J, resolveRelativePartName as K, parseClrMap as L, resolveSlideColorContext as M, resolveSlideThemeParts as N, resolveColorElement as O, applyColorTransforms as P, createElement as Q, parseClrScheme as R, Shape as S, TextFrame as T, partNameToZipPath as U, styleRefFill as V, relativePartName as W, boolValue as X, attr as Y, childElements$2 as Z, ChartAxis as _, isGraphicFrame as a, intValue as at, Picture as b, wrapShapeElement as c, removeAttr as ct, TableCell as d, setAttr as dt, firstChild as et, TableRow as f, Chart as g, ChartExSeries as h, isConnector as i, insertInOrder as it, resolveNotesColorContext as j, resolveLayoutColorContext as k, GraphicFrame as l, removeChildrenByQName as lt, ChartExAxis as m, buildShapes as n, getElements as nt, isGroupShape as o, ownerDocumentOf as ot, ChartEx as p, zipPathToPartName as q, isAutoShape as r, getOrAddChild as rt, isPicture as s, parseXml as st, GroupShape as t, firstChildElement as tt, Table as u, serializeXml as ut, ChartSeries as v, Run as w, AutoShape as x, Connector as y, remapLiteralColors as z };
5887
+ export { resolveColor as $, resolveColorElement as A, getOrAddChild as At, placeholderInheritedColor as B, Paragraph as C, childElements as Ct, readPatternFill as D, firstChild as Dt, spPrXfrmEmu as E, escapeXmlAttribute as Et, resolveSlideThemeParts as F, removeAttr as Ft, slideDefinesColor as G, placeholderInheritedRunProps as H, applyColorTransforms as I, removeChildrenByQName as It, fmtEntry as J, slideDefinesProp as K, LST_STYLE_LEVELS as L, replaceInParent as Lt, resolveMasterColorContext as M, intValue as Mt, resolveNotesColorContext as N, ownerDocumentOf as Nt, readPictureFill as O, firstChildElement as Ot, resolveSlideColorContext as P, parseXml as Pt, parseClrScheme as Q, RUN_PROP_NAMES as R, serializeXml as Rt, Shape as S, boolValue as St, TextFrame as T, descendantsByTag as Tt, placeholderInheritedXfrm as U, placeholderInheritedListStyles as V, placeholderOf as W, isA as X, intAttr as Y, parseClrMap as Z, ChartAxis as _, resolveRelativePartName as _t, isGraphicFrame as a, RPR_FILL_AFTER as at, Picture as b, OOXML_NS as bt, wrapShapeElement as c, SPPR_EFFECT_AFTER as ct, TableCell as d, SPPR_SCENE3D_AFTER as dt, resolveSchemeToken as et, TableRow as f, SPPR_SP3D_AFTER as ft, Chart as g, relsPartNameFor as gt, ChartExSeries as h, relativePartName as ht, isConnector as i, FILL_CHOICES as it, resolveLayoutColorContext as j, insertInOrder as jt, readGradientFill as k, getElements as kt, GraphicFrame as l, SPPR_FILL_AFTER as lt, ChartExAxis as m, partNameToZipPath as mt, buildShapes as n, styleRefLine as nt, isGroupShape as o, SHAPE_AFTER_SPPR as ot, ChartEx as p, partNameExtension as pt, SCHEME_SLOTS as q, isAutoShape as r, substitutePhClr as rt, isPicture as s, SPPR_AFTER_XFRM as st, GroupShape as t, styleRefFill as tt, Table as u, SPPR_LN_AFTER as ut, ChartSeries as v, resolveSingleRel as vt, Run as w, createElement as wt, AutoShape as x, attr as xt, Connector as y, zipPathToPartName as yt, placeholderInheritedAnchor as z, setAttr as zt };
5364
5888
 
5365
- //# sourceMappingURL=shapes-BZU8UTW2.js.map
5889
+ //# sourceMappingURL=shapes-BWifcHDV.js.map