@shbernal/ts-pptx 4.1.0 → 4.2.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 (139) hide show
  1. package/README.md +11 -11
  2. package/dist/anim-presets-Qbgh8uut.js +55 -0
  3. package/dist/anim-presets-Qbgh8uut.js.map +1 -0
  4. package/dist/{autopage-D4kyVp11.js → autopage-DqZ37VpL.js} +11 -12
  5. package/dist/autopage-DqZ37VpL.js.map +1 -0
  6. package/dist/{binary-string-CKVAYI3M.js → binary-string-Ba8VM3b9.js} +3 -4
  7. package/dist/binary-string-Ba8VM3b9.js.map +1 -0
  8. package/dist/{body-insets-kVHmjGc6.js → body-insets-BDHymJvz.js} +7 -9
  9. package/dist/body-insets-BDHymJvz.js.map +1 -0
  10. package/dist/browser.d.ts +24 -16
  11. package/dist/browser.d.ts.map +1 -1
  12. package/dist/browser.js +19 -19
  13. package/dist/browser.js.map +1 -1
  14. package/dist/{image-size-DwDbakcl.js → chart-metadata-CwiTTcJq.js} +576 -538
  15. package/dist/chart-metadata-CwiTTcJq.js.map +1 -0
  16. package/dist/{clip-DHDlnHUq.d.ts → clip-ISfCMtWA.d.ts} +6 -10
  17. package/dist/clip-ISfCMtWA.d.ts.map +1 -0
  18. package/dist/{notes-BcT571R4.js → comments-CKIZYRbZ.js} +517 -389
  19. package/dist/comments-CKIZYRbZ.js.map +1 -0
  20. package/dist/{diagnostics-DiMmBU8P.d.ts → diagnostics-MoA4MBq5.d.ts} +52 -50
  21. package/dist/diagnostics-MoA4MBq5.d.ts.map +1 -0
  22. package/dist/{embedded-fonts-CI_YGW2d.d.ts → embedded-fonts-U7HIIhPO.d.ts} +4 -6
  23. package/dist/embedded-fonts-U7HIIhPO.d.ts.map +1 -0
  24. package/dist/{entry-compose-6hF9aTmT.d.ts → entry-compose-CiakSt-f.d.ts} +53 -74
  25. package/dist/entry-compose-CiakSt-f.d.ts.map +1 -0
  26. package/dist/{errors-Da2nXCJm.js → entry-errors-JMthgJ2g.js} +41 -25
  27. package/dist/entry-errors-JMthgJ2g.js.map +1 -0
  28. package/dist/{table-cell-margins-PDshga8T.js → enums-BP9wsb9Q.js} +42 -45
  29. package/dist/enums-BP9wsb9Q.js.map +1 -0
  30. package/dist/{enums-DQRpVOLh.d.ts → enums-PcXr3o3J.d.ts} +11 -12
  31. package/dist/{enums-DQRpVOLh.d.ts.map → enums-PcXr3o3J.d.ts.map} +1 -1
  32. package/dist/families.d.ts +24 -26
  33. package/dist/families.d.ts.map +1 -1
  34. package/dist/families.js +3 -4
  35. package/dist/{fit-CcXbg__l.js → fit-Bpmzep6O.js} +60 -52
  36. package/dist/fit-Bpmzep6O.js.map +1 -0
  37. package/dist/{font-metrics-CBGmhLo0.d.ts → font-metrics-DTppw4t3.d.ts} +61 -37
  38. package/dist/font-metrics-DTppw4t3.d.ts.map +1 -0
  39. package/dist/{grid-9B2w5EaY.js → grid-DUIMxjfl.js} +23 -26
  40. package/dist/grid-DUIMxjfl.js.map +1 -0
  41. package/dist/{html-dom-DvJmuHt2.js → html-dom-LZCgntnJ.js} +42 -43
  42. package/dist/html-dom-LZCgntnJ.js.map +1 -0
  43. package/dist/html.d.ts +53 -9
  44. package/dist/html.d.ts.map +1 -1
  45. package/dist/html.js +12 -13
  46. package/dist/html.js.map +1 -1
  47. package/dist/index.d.ts +21 -13
  48. package/dist/index.d.ts.map +1 -1
  49. package/dist/index.js +15 -15
  50. package/dist/index.js.map +1 -1
  51. package/dist/inspect.d.ts +14 -16
  52. package/dist/inspect.d.ts.map +1 -1
  53. package/dist/inspect.js +18 -19
  54. package/dist/inspect.js.map +1 -1
  55. package/dist/{text-anchor-BgLPbwbL.js → lock-attrs-CSQnFDHP.js} +62 -12
  56. package/dist/lock-attrs-CSQnFDHP.js.map +1 -0
  57. package/dist/math.d.ts +2 -4
  58. package/dist/math.d.ts.map +1 -1
  59. package/dist/math.js +12 -13
  60. package/dist/math.js.map +1 -1
  61. package/dist/measure.d.ts +23 -12
  62. package/dist/measure.d.ts.map +1 -1
  63. package/dist/measure.js +3 -3
  64. package/dist/node.d.ts +21 -13
  65. package/dist/node.d.ts.map +1 -1
  66. package/dist/node.js +12 -12
  67. package/dist/node.js.map +1 -1
  68. package/dist/{options-internal-WqgMf720.js → options-internal-BWgmLPdx.js} +4 -5
  69. package/dist/options-internal-BWgmLPdx.js.map +1 -0
  70. package/dist/{package-KoEgVwII.d.ts → package-CANBWuSm.d.ts} +4 -12
  71. package/dist/package-CANBWuSm.d.ts.map +1 -0
  72. package/dist/{playbtn-CUqQdoDf.js → playbtn-BQZFJir9.js} +4 -5
  73. package/dist/playbtn-BQZFJir9.js.map +1 -0
  74. package/dist/{pres-BA9_lJbY.d.ts → pres-BkEkUAEM.d.ts} +383 -230
  75. package/dist/pres-BkEkUAEM.d.ts.map +1 -0
  76. package/dist/{presentation-Cyl74rq2.js → presentation-4Ggq0_Xa.js} +408 -288
  77. package/dist/presentation-4Ggq0_Xa.js.map +1 -0
  78. package/dist/{presentation-types-B3VQH4Bz.d.ts → presentation-types-BgnZR-0a.d.ts} +1104 -457
  79. package/dist/presentation-types-BgnZR-0a.d.ts.map +1 -0
  80. package/dist/read.d.ts +18 -13
  81. package/dist/read.d.ts.map +1 -1
  82. package/dist/read.js +4 -4
  83. package/dist/script.d.ts +116 -65
  84. package/dist/script.d.ts.map +1 -1
  85. package/dist/script.js +2697 -777
  86. package/dist/script.js.map +1 -1
  87. package/dist/{shapes-BcyFdZpp.js → shapes-CAPOjU0Q.js} +1556 -487
  88. package/dist/shapes-CAPOjU0Q.js.map +1 -0
  89. package/dist/{slide-BqPtyKmb.d.ts → slide-BmT4kMoO.d.ts} +94 -86
  90. package/dist/slide-BmT4kMoO.d.ts.map +1 -0
  91. package/dist/{slide-size-BalTjFuv.js → slide-size-SkScz1ig.js} +2 -3
  92. package/dist/{slide-size-BalTjFuv.js.map → slide-size-SkScz1ig.js.map} +1 -1
  93. package/dist/{table-dom-CVbvUfgh.js → table-dom-BYga4FYY.js} +3 -4
  94. package/dist/{table-dom-CVbvUfgh.js.map → table-dom-BYga4FYY.js.map} +1 -1
  95. package/dist/{text-BvFFMMFQ.js → text-CU08xJGs.js} +365 -318
  96. package/dist/text-CU08xJGs.js.map +1 -0
  97. package/dist/{transition-speed-kUEWDWTx.js → transition-speed-CNJ0fSpQ.js} +2 -3
  98. package/dist/{transition-speed-kUEWDWTx.js.map → transition-speed-CNJ0fSpQ.js.map} +1 -1
  99. package/dist/{units-internal-DFcUYjh3.js → units-internal-BR3a4cIi.js} +26 -29
  100. package/dist/units-internal-BR3a4cIi.js.map +1 -0
  101. package/dist/{zip-CYZ0F6BV.js → zip-7JTh61LX.js} +8 -10
  102. package/dist/zip-7JTh61LX.js.map +1 -0
  103. package/dist/{zip-CTll_dr0.d.ts → zip-uUjs9Cqx.d.ts} +4 -6
  104. package/dist/zip-uUjs9Cqx.d.ts.map +1 -0
  105. package/dist/zip.d.ts +3 -3
  106. package/dist/zip.js +3 -3
  107. package/dist/{zoom-BoNyOlPm.js → zoom-CSSeOwM8.js} +1153 -583
  108. package/dist/zoom-CSSeOwM8.js.map +1 -0
  109. package/package.json +20 -21
  110. package/skills/{ts-pptx-upstream → pptx-ts-upstream}/SKILL.md +27 -74
  111. package/dist/autopage-D4kyVp11.js.map +0 -1
  112. package/dist/binary-string-CKVAYI3M.js.map +0 -1
  113. package/dist/body-insets-kVHmjGc6.js.map +0 -1
  114. package/dist/clip-DHDlnHUq.d.ts.map +0 -1
  115. package/dist/diagnostics-DiMmBU8P.d.ts.map +0 -1
  116. package/dist/embedded-fonts-CI_YGW2d.d.ts.map +0 -1
  117. package/dist/entry-compose-6hF9aTmT.d.ts.map +0 -1
  118. package/dist/errors-Da2nXCJm.js.map +0 -1
  119. package/dist/fit-CcXbg__l.js.map +0 -1
  120. package/dist/font-metrics-CBGmhLo0.d.ts.map +0 -1
  121. package/dist/grid-9B2w5EaY.js.map +0 -1
  122. package/dist/html-dom-DvJmuHt2.js.map +0 -1
  123. package/dist/image-size-DwDbakcl.js.map +0 -1
  124. package/dist/notes-BcT571R4.js.map +0 -1
  125. package/dist/options-internal-WqgMf720.js.map +0 -1
  126. package/dist/package-KoEgVwII.d.ts.map +0 -1
  127. package/dist/playbtn-CUqQdoDf.js.map +0 -1
  128. package/dist/pres-BA9_lJbY.d.ts.map +0 -1
  129. package/dist/presentation-Cyl74rq2.js.map +0 -1
  130. package/dist/presentation-types-B3VQH4Bz.d.ts.map +0 -1
  131. package/dist/shapes-BcyFdZpp.js.map +0 -1
  132. package/dist/slide-BqPtyKmb.d.ts.map +0 -1
  133. package/dist/table-cell-margins-PDshga8T.js.map +0 -1
  134. package/dist/text-BvFFMMFQ.js.map +0 -1
  135. package/dist/text-anchor-BgLPbwbL.js.map +0 -1
  136. package/dist/units-internal-DFcUYjh3.js.map +0 -1
  137. package/dist/zip-CTll_dr0.d.ts.map +0 -1
  138. package/dist/zip-CYZ0F6BV.js.map +0 -1
  139. package/dist/zoom-BoNyOlPm.js.map +0 -1
@@ -1,25 +1,25 @@
1
- import { l as warn, n as InvalidOptionError, o as UnsupportedFeatureError, t as InternalError, u as warnOnce } from "./errors-Da2nXCJm.js";
2
- import { B as DEF_TEXT_SHADOW, G as MAX_FONT_SIZE_PT, I as DEF_SHAPE_LINE_COLOR, N as DEF_FONT_COLOR, S as transparencyToAlpha, U as LEVEL_MARGINS_EMU, W as LEVEL_PPR_TAIL, a as convertRotationDegrees, c as inch2Emu, f as opacityToAlpha, g as ptsToEmuLenient, h as positiveCoordinateEmu, i as convertArcAngle, l as lineWidthToEmu, n as clampRangedInput, p as percentToFixedPercent, q as SLDNUMFLDID, r as convertAngleUnits, s as getSmartParseNumber, u as mapStated, z as DEF_TEXT_GLOW } from "./units-internal-DFcUYjh3.js";
3
- import { F as EMU_PER_INCH, I as EMU_PER_POINT, P as ptToHundredths, b as FIXED_PCT_PER_PERCENT, c as SchemeColor, p as VALID_SHAPE_PRESETS, w as PERCENT_SCALE } from "./table-cell-margins-PDshga8T.js";
4
- import { Ct as FONT_REL, E as EMBEDDED_FONT_SLOTS, F as LINE_END_TYPES, G as TEXT_CAPS_TYPES, I as PATTERN_PRESETS, J as TEXT_STRIKE_TYPES, L as PRESET_LINE_DASHES, Mt as NOTES_MASTER_REL, W as TEXT_AUTONUM_SCHEMES, X as TEXT_UNDERLINE_TYPES, Xt as SLIDE_REL, Y as TEXT_TAB_ALIGN_TYPES, a as textAlignToken, an as OOXML_NS, c as checkEnumOrThrow, d as xsdBool, en as THEME_REL, f as xsdBoolIfTrue, h as stripHash, l as checkEnumOrWarn, m as splitRgbaHex, on as PML_ROOT_NS, p as isHexColor, s as lineCapToken, w as DEFAULT_COLOR_MAP, wt as HYPERLINK_REL, zt as PACKAGE_REL_NS } from "./image-size-DwDbakcl.js";
5
- import { n as setOrClear } from "./options-internal-WqgMf720.js";
6
- //#region src/xml-escape.ts
1
+ import { d as warnOnce, n as InternalError, r as InvalidOptionError, s as UnsupportedFeatureError, u as warn } from "./entry-errors-JMthgJ2g.js";
2
+ import { B as DEF_TEXT_SHADOW, G as MAX_FONT_SIZE_PT, I as DEF_SHAPE_LINE_COLOR, N as DEF_FONT_COLOR, S as transparencyToAlpha, U as LEVEL_MARGINS_EMU, W as LEVEL_PPR_TAIL, a as convertRotationDegrees, c as inch2Emu, f as opacityToAlpha, g as ptsToEmuLenient, h as positiveCoordinateEmu, i as convertArcAngle, l as lineWidthToEmu, n as clampRangedInput, p as percentToFixedPercent, q as SLDNUMFLDID, r as convertAngleUnits, s as getSmartParseNumber, u as mapStated, z as DEF_TEXT_GLOW } from "./units-internal-BR3a4cIi.js";
3
+ import { F as EMU_PER_INCH, I as EMU_PER_POINT, P as ptToHundredths, b as FIXED_PCT_PER_PERCENT, f as VALID_SHAPE_PRESETS, s as SchemeColor, w as PERCENT_SCALE } from "./enums-BP9wsb9Q.js";
4
+ import { A as FONT_REL, Ct as DEFAULT_COLOR_MAP, Ft as PATTERN_PRESETS, Gt as TEXT_CAPS_TYPES, Jt as TEXT_STRIKE_TYPES, K as PACKAGE_REL_NS, Lt as PRESET_LINE_DASHES, Pt as LINE_END_TYPES, Tt as EMBEDDED_FONT_SLOTS, Wt as TEXT_AUTONUM_SCHEMES, Xt as TEXT_UNDERLINE_TYPES, Yt as TEXT_TAB_ALIGN_TYPES, an as checkEnumOrWarn, c as SHOW_JUMPS, cn as xsdBoolIfTrue, dn as stripHash, ft as OOXML_NS, in as checkEnumOrThrow, j as HYPERLINK_REL, ln as isHexColor, pt as PML_ROOT_NS, rt as SLIDE_REL, s as textAlignToken, sn as xsdBool, st as THEME_REL, u as lineCapToken, un as splitRgbaHex, z as NOTES_MASTER_REL } from "./chart-metadata-CwiTTcJq.js";
5
+ import { n as setOrClear } from "./options-internal-BWgmLPdx.js";
6
+ // src/xml-escape.ts
7
7
  /**
8
8
  * pptx-ts: turning a caller's string into something that can sit inside XML.
9
9
  *
10
- * Three escapers with three notions of what is dangerous used to live here-and-there — one under
11
- * `gen/`, one under `read/oxml/`, and a fourth-of-a-job local one in `embedded-fonts.ts` — and the
10
+ * Three escapers with three notions of what is dangerous used to live here-and-there (one under
11
+ * `gen/`, one under `read/oxml/`, and a fourth-of-a-job local one in `embedded-fonts.ts`), and the
12
12
  * weakest was the one reached by the least-checked input. `serializeEmbeddedFontLst` puts
13
13
  * `font.typeface` straight from `pptx.embedFont({ typeface })` through an escaper that handled
14
14
  * `& < > "` and nothing else, so `embedFont({ typeface: 'Foo\v' })` wrote a vertical tab into
15
- * `<p:font typeface="…">` — a character XML 1.0 forbids outright, and exactly what the write side
16
- * strips at every other emission site — and `'Foo\nBar'` lost its newline to attribute-value
15
+ * `<p:font typeface="…">`, a character XML 1.0 forbids outright and exactly what the write side
16
+ * strips at every other emission site, and `'Foo\nBar'` lost its newline to attribute-value
17
17
  * normalisation.
18
18
  *
19
19
  * The copies had a real reason: `embedded-fonts.ts` is imported by `read/api/ops/`, so it must not
20
20
  * pull in `src/gen/`, and `read/oxml/dom.ts` is on the same side of that line. The answer to
21
21
  * "both halves need this" is a dependency-free root module, the same placement and rationale as
22
- * `hex-color.ts` — not a third, weaker copy.
22
+ * `hex-color.ts`, not a third, weaker copy.
23
23
  *
24
24
  * This module imports nothing.
25
25
  */
@@ -51,13 +51,13 @@ function hasIllegalXmlChars(value) {
51
51
  *
52
52
  * ELEMENT TEXT ONLY. A literal tab, carriage return or line feed is left alone here because in
53
53
  * character data it *is* the content (`<a:t>` line breaks depend on it). Inside an attribute value
54
- * the same three characters are destroyed by the parser rather than preserved — see
54
+ * the same three characters are destroyed by the parser rather than preserved; see
55
55
  * {@link encodeXmlAttrValue}, which every attribute-emitting path must use instead.
56
56
  * @param {string | number} xml - value to encode (numbers are stringified, as callers pass counts/sizes)
57
57
  * @returns {string} escaped XML
58
58
  */
59
59
  function encodeXmlEntities(xml) {
60
- if (typeof xml === "undefined" || xml == null) return "";
60
+ if (xml == null) return "";
61
61
  return xml.toString().replace(ILLEGAL_XML_CHARS_G, "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
62
62
  }
63
63
  /**
@@ -78,8 +78,7 @@ function encodeXmlEntities(xml) {
78
78
  function encodeXmlAttrValue(xml) {
79
79
  return encodeXmlEntities(xml).replace(/\t/g, "&#9;").replace(/\n/g, "&#10;").replace(/\r/g, "&#13;");
80
80
  }
81
- //#endregion
82
- //#region src/gen/utils.ts
81
+ // src/gen/utils.ts
83
82
  /**
84
83
  * pptx-ts: shared generator helpers.
85
84
  *
@@ -176,11 +175,11 @@ function getDuplicateObjectNames(names) {
176
175
  *
177
176
  * Counting is the normal case: a slide's rels are minted in order, so one past the
178
177
  * total is free. It is not enough on its own, because a rel can arrive on a slide
179
- * carrying an id minted somewhere else — auto-paging re-registers a repeated header
178
+ * carrying an id minted somewhere else: auto-paging re-registers a repeated header
180
179
  * row's hyperlink on each overflow slide under the id stamped on the shared
181
180
  * hyperlink object (`gen/define/hyperlinks.ts`). That id is then held but not
182
181
  * counted-to, so the next mint could hand out the same number and the part would
183
- * declare two `Relationship` elements sharing an `Id` — invalid OPC, and PowerPoint
182
+ * declare two `Relationship` elements sharing an `Id`. That is invalid OPC, and PowerPoint
184
183
  * resolves such a reference to whichever comes first. Hence the scan: start where
185
184
  * the count says, then step over anything the slide already holds.
186
185
  * @param {PresSlideInternal} target - the slide to use
@@ -237,7 +236,7 @@ function mediaSlideKey(target) {
237
236
  * `embeddings`.
238
237
  *
239
238
  * The trailing number counts a target's media parts from one, and it is read *before* the
240
- * push that consumes the name — so this must be called in the object literal being pushed,
239
+ * push that consumes the name, so this must be called in the object literal being pushed,
241
240
  * not hoisted above an earlier push, which would rename every part after it. Eight sites
242
241
  * spelled the rule out by hand with `_relsMedia.length + 1` inline; naming it is what lets
243
242
  * the off-by-one convention be stated once and read.
@@ -252,7 +251,7 @@ function nextMediaTarget(target, kind, extn, dir = "media") {
252
251
  }
253
252
  /**
254
253
  * The `path` a media rel carries when its bytes came inline as `data` and there is no file to
255
- * name — `preencoded.png`, `preencoded.mp4`. Nothing loads it: the media pass tells it apart
254
+ * name: `preencoded.png`, `preencoded.mp4`. Nothing loads it: the media pass tells it apart
256
255
  * with {@link isPreencodedPath}.
257
256
  *
258
257
  * The spelling still reaches a deck. An image object with no alt text falls back to its path for
@@ -271,8 +270,7 @@ function preencodedPath(extn) {
271
270
  function isPreencodedPath(path) {
272
271
  return path.startsWith("preencoded.");
273
272
  }
274
- //#endregion
275
- //#region src/ooxml/xml-decl.ts
273
+ // src/ooxml/xml-decl.ts
276
274
  /**
277
275
  * The XML prolog every OOXML part this library writes begins with, including the parts the read
278
276
  * side regenerates when it serializes a package it has changed.
@@ -283,94 +281,12 @@ function isPreencodedPath(path) {
283
281
  * import: this module imports nothing, and no `ooxml/` module imports from `gen/` or `read/`.
284
282
  */
285
283
  const XML_DECL = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>";
286
- //#endregion
287
- //#region src/embedded-fonts.ts
288
- /**
289
- * Shared model + serializer for PresentationML embedded fonts, used by both the
290
- * write side (author-side `pptx.embedFont`, `gen/pres/presentation.ts`) and the read side
291
- * (the `embedFonts` carry in `importSlide`, `importSlides`, `importSlideMasters` and
292
- * `appendSlides`, `src/read/api/ops/embedded-fonts.ts`).
293
- *
294
- * Embedded fonts are three coordinated pieces (ECMA-376 transitional):
295
- * 1. binary font parts `/ppt/fonts/fontN.fntdata` — the **raw** TTF/OTF bytes
296
- * (PresentationML does not obfuscate embedded fonts), content type
297
- * `application/x-fontdata` via a single `fntdata` Default extension;
298
- * 2. one `font` relationship per face from `presentation.xml`;
299
- * 3. a `p:embeddedFontLst` in `presentation.xml`, at index 7 of the
300
- * `CT_Presentation` child sequence (after `smartTags`, before `custShowLst`
301
- * … `defaultTextStyle`).
302
- *
303
- * This module owns only the OOXML-shape knowledge; rId allocation and part
304
- * placement stay with each caller (their packaging models differ).
305
- *
306
- * It must not import from `src/gen/`, because `read/api/ops/embedded-fonts.ts` imports it. What
307
- * it needs from elsewhere -- the attribute escaper, the `font` relationship type and the face slot
308
- * list -- comes from modules that import nothing outside `src/ooxml/` themselves.
309
- */
310
- /** Extension for the binary font parts (`/ppt/fonts/fontN.fntdata`). */
311
- const FONT_DATA_EXTENSION = "fntdata";
312
- /** Content type for a `.fntdata` part — one `Default` covers every font part. */
313
- const FONT_DATA_CONTENT_TYPE = "application/x-fontdata";
314
- /** Relationship type from `presentation.xml` to a font part (one per face). */
315
- const FONT_REL_TYPE = FONT_REL;
316
- /**
317
- * Flatten `fonts` into one ordered face list, assigning sequential part indices
318
- * (1-based) and relationship ids (starting at `firstRId`). Faces are emitted in
319
- * `fonts` order, each font's faces in {@link EMBEDDED_FONT_SLOTS} order. Faces
320
- * without `bytes` are skipped (nothing to write).
321
- */
322
- function flattenEmbeddedFaces(fonts, firstRId) {
323
- const flat = [];
324
- let partIndex = 0;
325
- let rId = firstRId;
326
- fonts.forEach((font, fontIndex) => {
327
- for (const slot of EMBEDDED_FONT_SLOTS) {
328
- const face = font.faces.find((f) => f.slot === slot);
329
- if (!face || !face.bytes) continue;
330
- flat.push({
331
- fontIndex,
332
- slot,
333
- bytes: face.bytes,
334
- partIndex: ++partIndex,
335
- rId: rId++
336
- });
337
- }
338
- });
339
- return flat;
340
- }
341
- /**
342
- * Serialize a `<p:embeddedFontLst>` from `fonts` and a `(fontIndex, slot) → rId`
343
- * lookup (typically backed by {@link flattenEmbeddedFaces}). Returns `''` when no
344
- * font has any face with an allocated rId, so callers can emit nothing. Assumes
345
- * the enclosing document declares the `p:` and `r:` prefixes (presentation.xml
346
- * does), so no namespace declarations are emitted here.
347
- */
348
- function serializeEmbeddedFontLst(fonts, rIdForFace) {
349
- const entries = [];
350
- fonts.forEach((font, fontIndex) => {
351
- const faceXml = [];
352
- for (const slot of EMBEDDED_FONT_SLOTS) {
353
- const rId = rIdForFace(fontIndex, slot);
354
- if (rId === void 0) continue;
355
- faceXml.push(`<p:${slot} r:id="rId${rId}"/>`);
356
- }
357
- if (faceXml.length === 0) return;
358
- let fontAttrs = `typeface="${encodeXmlAttrValue(font.typeface)}"`;
359
- if (font.panose !== void 0) fontAttrs += ` panose="${encodeXmlAttrValue(font.panose)}"`;
360
- if (font.pitchFamily !== void 0) fontAttrs += ` pitchFamily="${font.pitchFamily}"`;
361
- if (font.charset !== void 0) fontAttrs += ` charset="${font.charset}"`;
362
- entries.push(`<p:embeddedFont><p:font ${fontAttrs}/>${faceXml.join("")}</p:embeddedFont>`);
363
- });
364
- if (entries.length === 0) return "";
365
- return `<p:embeddedFontLst>${entries.join("")}</p:embeddedFontLst>`;
366
- }
367
- //#endregion
368
- //#region src/gen/oxml/el.ts
284
+ // src/gen/oxml/el.ts
369
285
  /**
370
286
  * pptx-ts: XML element builder (write-side substrate)
371
287
  *
372
288
  * The write path historically concatenated template strings and called
373
- * `encodeXmlEntities` by hand at every interpolation — the source of escaping,
289
+ * `encodeXmlEntities` by hand at every interpolation, the source of escaping,
374
290
  * attribute-order and child-sequence bugs. This builder centralizes escaping
375
291
  * while reproducing today's exact byte layout, so a migration onto it can be
376
292
  * gated on strict byte-identity (`pnpm run byte-identity:check`) rather than on
@@ -383,7 +299,7 @@ function serializeEmbeddedFontLst(fonts, rIdForFace) {
383
299
  * 1. Self-closing is decided by ARITY, never by child value. `el()` always emits
384
300
  * a paired tag; `voidEl()` always self-closes. This matters because
385
301
  * `encodeXmlEntities(undefined)` returns `''`, so a value-based rule would
386
- * turn today's `<dc:title></dc:title>` into `<dc:title/>` — a silent byte
302
+ * turn today's `<dc:title></dc:title>` into `<dc:title/>`, a silent byte
387
303
  * regression on every optional-text element in the tree.
388
304
  *
389
305
  * Escaping is split to match: attribute values go through `encodeXmlAttrValue`
@@ -413,7 +329,7 @@ function openTag(name, attrs, fmt) {
413
329
  return out;
414
330
  }
415
331
  /**
416
- * Paired element: `<name …>children</name>` — emitted even when `children` is
332
+ * Paired element: `<name …>children</name>`, emitted even when `children` is
417
333
  * empty or `undefined`. Text children are escaped; `raw()` children are not.
418
334
  */
419
335
  function el(name, attrs, children, fmt) {
@@ -426,7 +342,7 @@ function el(name, attrs, children, fmt) {
426
342
  return out + (fmt?.closePrefix ?? "") + "</" + name + ">";
427
343
  }
428
344
  /**
429
- * Self-closing element: `<name …/>`. `fmt.closePrefix` goes before the `/>` — a
345
+ * Self-closing element: `<name …/>`. `fmt.closePrefix` goes before the `/>`. A
430
346
  * handful of emitters write `<a:avLst />` with a space there, and that space is
431
347
  * byte-significant like any other.
432
348
  */
@@ -445,8 +361,7 @@ function voidEl(name, attrs, fmt) {
445
361
  function elOrVoid(name, attrs, childXml, fmt) {
446
362
  return childXml ? el(name, attrs, raw(childXml), fmt) : voidEl(name, attrs, fmt);
447
363
  }
448
- //#endregion
449
- //#region src/gen/drawingml/color.ts
364
+ // src/gen/drawingml/color.ts
450
365
  /**
451
366
  * pptx-ts: DrawingML color elements
452
367
  *
@@ -467,7 +382,7 @@ function elOrVoid(name, attrs, childXml, fmt) {
467
382
  */
468
383
  const ALPHA_TAG = "<a:alpha";
469
384
  /**
470
- * Emit a bare DrawingML color element — either `<a:schemeClr>` (scheme color) or `<a:srgbClr>`
385
+ * Emit a bare DrawingML color element: either `<a:schemeClr>` (scheme color) or `<a:srgbClr>`
471
386
  * (hex RGB). This is a low-level primitive: it is the shared building block for every color
472
387
  * context (solid fills, gradient stops, `<a:alpha>` on shadows/glow, line fills, highlight,
473
388
  * underline, patterns, …), so it is intentionally NOT limited to solid fills and cannot be
@@ -475,7 +390,7 @@ const ALPHA_TAG = "<a:alpha";
475
390
  *
476
391
  * When you specifically want a *solid fill*, prefer `genXmlColorSelection` (`fill.ts`), which
477
392
  * wraps this in `<a:solidFill>…</a:solidFill>` and also handles `alpha`/`transparency` and the
478
- * gradient/pattern/image fill types — reach for `createColorElement` directly only when you
393
+ * gradient/pattern/image fill types. Reach for `createColorElement` directly only when you
479
394
  * need the raw color element (e.g. inside `<a:ln>`, `<a:gs>`, an effect, or a highlight).
480
395
  * @param {string|SCHEME_COLORS} colorStr - hex RGB (e.g. "FFFF00") or a scheme color constant (e.g. SchemeColor.ACCENT1)
481
396
  * @param {string} [innerElements] - additional elements that adjust the color, nested inside the color element
@@ -490,7 +405,7 @@ function createColorElement(colorStr, innerElements) {
490
405
  warnOnce("color/empty-string", `An empty string is not a color. "${DEF_FONT_COLOR}" used instead: this position requires one, so there is nothing to inherit. Omit the whole option if you meant to paint nothing.`);
491
406
  colorStr = DEF_FONT_COLOR;
492
407
  }
493
- let colorVal = stripHash(colorStr || "");
408
+ let colorVal = stripHash(colorStr);
494
409
  const rgba = splitRgbaHex(colorVal);
495
410
  if (rgba.alpha !== void 0) {
496
411
  if (!innerElements?.includes(ALPHA_TAG)) innerElements = alphaEl(Math.round(rgba.alpha * PERCENT_SCALE)) + (innerElements || "");
@@ -521,8 +436,8 @@ function isColorValue(value) {
521
436
  *
522
437
  * `''` is not a spelling of "no paint": omission already spells that, and `'inherit'` and
523
438
  * `'none'` spell the other two states a paint can be in. It reaches the library from the
524
- * caller's own missing value — an unset template field, `row.accent` on a row that has none
525
- * — so the useful answer is to say so and then behave exactly as omitting the option would
439
+ * caller's own missing value (an unset template field, `row.accent` on a row that has none),
440
+ * so the useful answer is to say so and then behave exactly as omitting the option would
526
441
  * have. What it must not do is coerce to {@link DEF_FONT_COLOR}, which paints visible black
527
442
  * on a shape the caller expected to keep the theme's paint.
528
443
  *
@@ -536,7 +451,7 @@ function isColorValue(value) {
536
451
  */
537
452
  function rejectEmptyColor(value, option) {
538
453
  if (value !== "") return false;
539
- warnOnce("color/empty-string", `\`${option}\` is an empty string, which is not a color — the option was ignored, exactly as omitting it would be. Pass 6-digit hex RGB (e.g. "FF3399") or a SchemeColor value.`);
454
+ warnOnce("color/empty-string", `\`${option}\` is an empty string, which is not a color, so the option was ignored, exactly as omitting it would be. Pass 6-digit hex RGB (e.g. "FF3399") or a SchemeColor value.`);
540
455
  return true;
541
456
  }
542
457
  /**
@@ -544,7 +459,7 @@ function rejectEmptyColor(value, option) {
544
459
  *
545
460
  * The `x ?? DEFAULT` and `x || DEFAULT` this replaces disagreed about `''`: the first passed
546
461
  * it through to be painted black or dropped, the second quietly resolved it to the default.
547
- * Here it is one rule — an empty string is a missing value, reported and then resolved the
462
+ * Here it is one rule: an empty string is a missing value, reported and then resolved the
548
463
  * way an absent option is.
549
464
  * @param value - the caller's colour option, possibly absent or empty
550
465
  * @param fallback - what an absent option resolves to at this site
@@ -565,126 +480,7 @@ function namedColorOr(value, fallback, option) {
565
480
  function alphaEl(fixedPct) {
566
481
  return voidEl("a:alpha", { val: fixedPct });
567
482
  }
568
- //#endregion
569
- //#region src/gen/drawingml/effect.ts
570
- /**
571
- * pptx-ts: DrawingML effects (glow and shadow)
572
- *
573
- * Emit the `<a:glow>` / `<a:outerShdw>` / `<a:innerShdw>` children of an
574
- * `<a:effectLst>`, and normalize the user-facing `shadow` options into the
575
- * internal shape every emit site reads.
576
- * @see http://officeopenxml.com/drwSp-effects.php
577
- */
578
- /**
579
- * Creates `a:glow` element
580
- * @param {TextGlowProps} options glow properties
581
- * @param {TextGlowProps} defaults defaults for unspecified properties in `opts`
582
- * @see http://officeopenxml.com/drwSp-effects.php
583
- * { size: 8, color: 'FFFFFF', opacity: 0.75 };
584
- */
585
- function createGlowElement(options, defaults) {
586
- const opts = {
587
- ...defaults,
588
- ...options
589
- };
590
- const size = positiveCoordinateEmu(opts.size, "glow/size-out-of-range", "glow.size");
591
- const color = namedColorOr(opts.color, DEF_FONT_COLOR, "glow.color");
592
- const opacity = opacityToAlpha(opts.opacity ?? 0);
593
- return el("a:glow", { rad: size }, raw(createColorElement(color, alphaEl(opacity))));
594
- }
595
- /**
596
- * Creates an `a:outerShdw`/`a:innerShdw` element for a text run, shape, image, or chart.
597
- * Returns the shadow element only (no wrapping `a:effectLst`) so callers can either combine
598
- * it with other effects (e.g. glow) inside one `a:effectLst`, or wrap a lone shadow via
599
- * {@link createShadowEffectLst}.
600
- *
601
- * Colors go through {@link createColorElement}, so scheme colors (e.g. `accent1`) are honored
602
- * — earlier per-site copies hardcoded `a:srgbClr` and silently emitted invalid OOXML for them.
603
- * @param {ShadowPropsInternal} options shadow properties
604
- * @param {ShadowPropsInternal} defaults defaults for unspecified properties in `options`
605
- * @see http://officeopenxml.com/drwSp-effects.php
606
- * @returns {string} XML string, or '' when type is 'none'
607
- */
608
- function createShadowElement(options, defaults) {
609
- const opts = {
610
- ...defaults,
611
- ...options
612
- };
613
- if (opts.type === "none") return "";
614
- const type = opts.type || "outer";
615
- const blur = positiveCoordinateEmu(opts.blur ?? 0, "shadow/blur-out-of-range", "shadow.blur");
616
- const offset = positiveCoordinateEmu(opts.offset ?? 0, "shadow/offset-out-of-range", "shadow.offset");
617
- const angle = convertAngleUnits(opts.angle ?? 0, "shadow.angle");
618
- const opacity = Math.round((opts._alpha ?? .75) * PERCENT_SCALE);
619
- const color = namedColorOr(opts.color, DEF_FONT_COLOR, "shadow.color");
620
- const attrs = {
621
- ...type === "outer" ? {
622
- sx: 1e5,
623
- sy: 1e5,
624
- kx: 0,
625
- ky: 0,
626
- algn: "bl",
627
- rotWithShape: xsdBool(opts.rotateWithShape)
628
- } : {},
629
- blurRad: blur,
630
- dist: offset,
631
- dir: angle
632
- };
633
- return el(`a:${type}Shdw`, attrs, raw(createColorElement(color, alphaEl(opacity))));
634
- }
635
- /**
636
- * Wraps a lone shadow in the `a:effectLst` that CT_ShapeProperties (shapes/images) and chart
637
- * marker/data-point properties require. Returns a self-closing `<a:effectLst/>` when there is
638
- * no shadow (missing/non-object options, or `type: 'none'`), matching the "no effects" element
639
- * PowerPoint emits. Use this instead of {@link createShadowElement} when the shadow is the only
640
- * effect; use `createShadowElement` directly when combining with other effects (e.g. glow).
641
- * @param {ShadowPropsInternal} options shadow properties
642
- * @param {ShadowPropsInternal} defaults defaults for unspecified properties in `options`
643
- * @returns {string} `<a:effectLst>…</a:effectLst>` or `<a:effectLst/>`
644
- */
645
- function createShadowEffectLst(options, defaults) {
646
- if (!options || typeof options !== "object") return voidEl("a:effectLst");
647
- return elOrVoid("a:effectLst", null, createShadowElement(options, defaults));
648
- }
649
- /**
650
- * Normalize a caller's `shadow` into the internal shape every emit site reads: a legal `type`, a
651
- * whole-degree `angle` inside `ST_PositiveFixedAngle`, a `_alpha` derived from `transparency` or
652
- * from an RGBA colour's alpha byte, and a colour with no `#` and no alpha byte.
653
- *
654
- * **Pure.** It used to normalize the caller's object *in place* and return it, and its four call
655
- * sites split on which of the two contracts they used — two assigned the result, two discarded it
656
- * and relied on the mutation. That made the obvious cleanup a trap: making it pure without
657
- * converting every caller silently drops `transparency` and RGBA alpha on every text run, because
658
- * `_alpha` is what `genXmlTextRunProperties` reads and nothing else writes it. The value now
659
- * reaches the emitter as a value rather than as a side effect, and a shadow literal shared across
660
- * two shapes comes back untouched.
661
- * @param shadow - the caller's shadow options
662
- * @returns a fresh normalized bag, or `undefined` when the caller stated no shadow object
663
- */
664
- function normalizeShadowOptions(shadow) {
665
- if (!shadow || typeof shadow !== "object") return;
666
- const corrected = { ...shadow };
667
- if (corrected.type !== "outer" && corrected.type !== "inner" && corrected.type !== "none") {
668
- warn("shadow/invalid-type", "shadow.type options are `outer`, `inner` or `none`.");
669
- corrected.type = "outer";
670
- }
671
- if (corrected.angle !== void 0) corrected.angle = Math.round(clampRangedInput(corrected.angle, 0, 359, "shadow/angle-out-of-range", "shadow.angle", "shadow/angle-non-finite"));
672
- if (corrected.transparency !== void 0) corrected._alpha = 1 - clampRangedInput(corrected.transparency, 0, 100, "shadow/transparency-out-of-range", "shadow.transparency") / 100;
673
- if (corrected.color) {
674
- if (corrected.color.startsWith("#")) {
675
- warn("shadow/color-has-hash", "shadow.color should not include hash (#) character, , e.g. \"FF0000\"");
676
- corrected.color = stripHash(corrected.color);
677
- }
678
- const rgba = splitRgbaHex(corrected.color);
679
- if (rgba.alpha !== void 0) {
680
- if (corrected._alpha === void 0) corrected._alpha = rgba.alpha;
681
- corrected.color = rgba.rgb;
682
- }
683
- }
684
- return corrected;
685
- }
686
- //#endregion
687
- //#region src/gen/drawingml/src-rect.ts
483
+ // src/gen/drawingml/src-rect.ts
688
484
  /**
689
485
  * pptx-ts: the explicit `<a:srcRect>` source crop
690
486
  *
@@ -697,9 +493,10 @@ function normalizeShadowOptions(shadow) {
697
493
  * points, which never measure an image.
698
494
  */
699
495
  /**
700
- * The four insets, defaulted and checked. Rejects anything that is not a percentage and
701
- * any pair that would leave no source area at all — a degenerate `<a:srcRect>` renders as
702
- * an empty shape rather than as an error, so it has to fail here.
496
+ * The four insets, defaulted and checked. Rejects a non-finite inset, one over 100, and any
497
+ * pair that would leave no source area at all. A degenerate `<a:srcRect>` renders as an
498
+ * empty shape rather than as an error, so it has to fail here. A negative inset is accepted:
499
+ * DrawingML's `ST_Percentage` is signed, and it is how a source is padded inside its box.
703
500
  * @param crop - edge insets in percent; omitted edges default to 0
704
501
  * @param label - the option's owner, as an error message names it (`addImage`, `image fill`)
705
502
  * @param where - trailing context for an error message, or `''`
@@ -711,17 +508,18 @@ function normalizeImageCrop(crop, label, where) {
711
508
  r: crop.r ?? 0,
712
509
  b: crop.b ?? 0
713
510
  };
714
- for (const [name, val] of Object.entries(edges)) if (typeof val !== "number" || !Number.isFinite(val) || val < 0 || val > 100) throw new InvalidOptionError("image/crop-inset-out-of-range", `${label} crop.${name} must be a percentage between 0 and 100 (got ${String(val)})${where}.`);
511
+ for (const [name, val] of Object.entries(edges)) if (typeof val !== "number" || !Number.isFinite(val) || val > 100) throw new InvalidOptionError("image/crop-inset-out-of-range", `${label} crop.${name} must be a finite percentage no greater than 100 (got ${String(val)})${where}.`);
715
512
  if (edges.l + edges.r >= 100) throw new InvalidOptionError("image/crop-insets-exceed-extent", `${label} crop: left+right insets (${edges.l}%+${edges.r}%) must be < 100%${where}.`);
716
513
  if (edges.t + edges.b >= 100) throw new InvalidOptionError("image/crop-insets-exceed-extent", `${label} crop: top+bottom insets (${edges.t}%+${edges.b}%) must be < 100%${where}.`);
717
514
  return edges;
718
515
  }
719
516
  /**
720
- * Build an explicit `<a:srcRect>` crop from percentage edge insets (0–100), emitted verbatim.
721
- * Each inset is the percent of the *source* image trimmed from that edge; values serialize in
722
- * 1000ths of a percent (DrawingML `ST_Percentage`, where 100% = 100000). Unlike `sizing: 'crop'`
723
- * — which derives the rect from displayed inches and the natural pixel size — this maps a
724
- * sub-region of the source directly, so it works for SVG and other unmeasurable formats.
517
+ * Build an explicit `<a:srcRect>` crop from percentage edge insets, emitted verbatim.
518
+ * Each inset is the percent of the *source* image trimmed from that edge, or padded onto it
519
+ * when negative; values serialize in 1000ths of a percent (DrawingML `ST_Percentage`, where
520
+ * 100% = 100000). Unlike `sizing: 'crop'`, which derives the rect from displayed inches and the
521
+ * natural pixel size, this maps a sub-region of the source directly, so it works for SVG and
522
+ * other unmeasurable formats.
725
523
  * @param crop - edge insets in percent; omitted edges default to 0
726
524
  * @param label - the option's owner, as an error message names it
727
525
  * @param where - trailing context for an error message, or `''`
@@ -738,7 +536,7 @@ function genXmlImageCropRect(crop, label, where) {
738
536
  });
739
537
  }
740
538
  /**
741
- * `<a:stretch><a:fillRect/></a:stretch>` — the blipFill child that scales the source to the
539
+ * `<a:stretch><a:fillRect/></a:stretch>`: the blipFill child that scales the source to the
742
540
  * shape's box, which every picture-bearing emitter writes and none parameterizes.
743
541
  *
744
542
  * After an `<a:srcRect/>`, it completes the canonical picture fill ECMA-376 gives
@@ -750,8 +548,7 @@ function genXmlImageCropRect(crop, label, where) {
750
548
  * it from every shape, table and chart emitter, and must not pull the image-format tables in.
751
549
  */
752
550
  const STRETCH_FILL_RECT = el("a:stretch", null, raw(voidEl("a:fillRect")));
753
- //#endregion
754
- //#region src/gen/drawingml/fill.ts
551
+ // src/gen/drawingml/fill.ts
755
552
  /**
756
553
  * pptx-ts: DrawingML fills
757
554
  *
@@ -851,7 +648,7 @@ function genXmlImageFill(props) {
851
648
  * `type: 'none'` needs in order to mean anything (`{ type: 'none', color }` must stay
852
649
  * transparent), so it is the rule everywhere rather than one kind's exception.
853
650
  *
854
- * With no `type`, the first sub-object present selects the kind, in declaration order —
651
+ * With no `type`, the first sub-object present selects the kind, in declaration order:
855
652
  * `gradient`, then `pattern`, then `image`. Setting two without a `type` is already a
856
653
  * contradiction; the order exists so the answer is stated rather than emergent.
857
654
  *
@@ -873,15 +670,15 @@ function resolveFillKind(props) {
873
670
  /**
874
671
  * {@link resolveFillKind} for a stroke, which has one kind fewer.
875
672
  *
876
- * `<a:ln>`'s paint child is `EG_LineFillProperties` — `a:noFill`, `a:solidFill`, `a:gradFill`,
877
- * `a:pattFill` — with no `a:blipFill` among them, so a picture stroke has no OOXML expression
673
+ * `<a:ln>`'s paint child is `EG_LineFillProperties` (`a:noFill`, `a:solidFill`, `a:gradFill`,
674
+ * `a:pattFill`), with no `a:blipFill` among them, so a picture stroke has no OOXML expression
878
675
  * and `ShapeLineProps` subtracts `image` from the fill props it inherits. This is where the
879
676
  * subtraction is enforced at run time, for the JS caller TypeScript cannot stop: the request
880
677
  * is refused rather than painted as nothing, because emitting a blipFill inside `a:ln` is a
881
678
  * package PowerPoint reports as needing repair, and emitting nothing is a stroke the caller
882
679
  * asked to be a picture and got in the theme colour without being told.
883
680
  *
884
- * Every stroke site resolves through here — the two `define/` rebuilds and the emitter — so
681
+ * Every stroke site resolves through here (the two `define/` rebuilds and the emitter), so
885
682
  * the refusal names the call the caller made rather than surfacing at serialization time.
886
683
  */
887
684
  function resolveLineKind(props) {
@@ -897,7 +694,7 @@ function resolveLineKind(props) {
897
694
  * a fill child for an empty props object would paint them default black.
898
695
  *
899
696
  * "Said nothing" is narrower than "is empty". `'inherit'` is the explicit spelling of the
900
- * same silence and is false here. `'none'` is a statement — `<a:noFill/>` — and is true.
697
+ * same silence and is false here. `'none'` is a statement (`<a:noFill/>`) and is true.
901
698
  * A `'solid'` kind needs a `color` to be saying anything; every other kind carries its
902
699
  * payload in its own sub-object, so its presence is the statement.
903
700
  *
@@ -916,7 +713,7 @@ function fillNamesPaint(props) {
916
713
  /**
917
714
  * Reject the empty-colour spelling of a missing value, in either shape a fill can wear.
918
715
  *
919
- * `''` and `{ color: '' }` say the same thing — the caller had no colour — and only the
716
+ * `''` and `{ color: '' }` say the same thing (the caller had no colour), and only the
920
717
  * second used to reach {@link createColorElement} and paint black. Both fold back onto
921
718
  * *absence* here, which is not the same as "emit nothing": what an absent fill means is the
922
719
  * surface's own rule, and a text box's absent fill is `<a:noFill/>`. So a site whose absence
@@ -933,8 +730,8 @@ function rejectEmptyFill(props, option) {
933
730
  /**
934
731
  * A solid paint for {@link genXmlColorSelection}, carrying `transparency` only when one was set.
935
732
  *
936
- * Every caller assembles this out of an optional source — `border.transparency`,
937
- * `opts.transparency` — and writing the key with an `undefined` in it would be a second spelling
733
+ * Every caller assembles this out of an optional source (`border.transparency`,
734
+ * `opts.transparency`), and writing the key with an `undefined` in it would be a second spelling
938
735
  * of "no transparency": one that reads the same here, but not in a bag that is spread over
939
736
  * another. So the key is simply absent, which is this codebase's one spelling of unset.
940
737
  * @param color - the colour to paint, already resolved against whatever default applies
@@ -949,7 +746,7 @@ function solidPaint(color, transparency) {
949
746
  /**
950
747
  * Emit the fill group a set of fill/line props asks for, dispatching on
951
748
  * {@link resolveFillKind}. Returns '' for props that name nothing to paint, so a caller
952
- * with an optional paint can hand this whatever it has — including `undefined`.
749
+ * with an optional paint can hand this whatever it has, including `undefined`.
953
750
  * @param {Color | ShapeFillProps | ShapeLineProps | undefined} props fill props
954
751
  * @returns XML string
955
752
  */
@@ -989,8 +786,204 @@ function genXmlColorSelection(props) {
989
786
  }
990
787
  return outText;
991
788
  }
992
- //#endregion
993
- //#region src/gen/drawingml/line.ts
789
+ // src/embedded-fonts.ts
790
+ /**
791
+ * Shared model + serializer for PresentationML embedded fonts, used by both the
792
+ * write side (author-side `pptx.embedFont`, `gen/pres/presentation.ts`) and the read side
793
+ * (the `embedFonts` carry in `importSlide`, `importSlides`, `importSlideMasters` and
794
+ * `appendSlides`, `src/read/api/ops/embedded-fonts.ts`).
795
+ *
796
+ * Embedded fonts are three coordinated pieces (ECMA-376 transitional):
797
+ * 1. binary font parts `/ppt/fonts/fontN.fntdata`, holding the **raw** TTF/OTF bytes
798
+ * (PresentationML does not obfuscate embedded fonts), content type
799
+ * `application/x-fontdata` via a single `fntdata` Default extension;
800
+ * 2. one `font` relationship per face from `presentation.xml`;
801
+ * 3. a `p:embeddedFontLst` in `presentation.xml`, at index 7 of the
802
+ * `CT_Presentation` child sequence (after `smartTags`, before `custShowLst`
803
+ * … `defaultTextStyle`).
804
+ *
805
+ * This module owns only the OOXML-shape knowledge; rId allocation and part
806
+ * placement stay with each caller (their packaging models differ).
807
+ *
808
+ * It must not import from `src/gen/`, because `read/api/ops/embedded-fonts.ts` imports it. What
809
+ * it needs from elsewhere -- the attribute escaper, the `font` relationship type and the face slot
810
+ * list -- comes from modules that import nothing outside `src/ooxml/` themselves.
811
+ */
812
+ /** Extension for the binary font parts (`/ppt/fonts/fontN.fntdata`). */
813
+ const FONT_DATA_EXTENSION = "fntdata";
814
+ /** Content type for a `.fntdata` part. One `Default` covers every font part. */
815
+ const FONT_DATA_CONTENT_TYPE = "application/x-fontdata";
816
+ /** Relationship type from `presentation.xml` to a font part (one per face). */
817
+ const FONT_REL_TYPE = FONT_REL;
818
+ /**
819
+ * Flatten `fonts` into one ordered face list, assigning sequential part indices
820
+ * (1-based) and relationship ids (starting at `firstRId`). Faces are emitted in
821
+ * `fonts` order, each font's faces in {@link EMBEDDED_FONT_SLOTS} order. Faces
822
+ * without `bytes` are skipped (nothing to write).
823
+ */
824
+ function flattenEmbeddedFaces(fonts, firstRId) {
825
+ const flat = [];
826
+ let partIndex = 0;
827
+ let rId = firstRId;
828
+ fonts.forEach((font, fontIndex) => {
829
+ for (const slot of EMBEDDED_FONT_SLOTS) {
830
+ const face = font.faces.find((f) => f.slot === slot);
831
+ if (!face || !face.bytes) continue;
832
+ flat.push({
833
+ fontIndex,
834
+ slot,
835
+ bytes: face.bytes,
836
+ partIndex: ++partIndex,
837
+ rId: rId++
838
+ });
839
+ }
840
+ });
841
+ return flat;
842
+ }
843
+ /**
844
+ * Serialize a `<p:embeddedFontLst>` from `fonts` and a `(fontIndex, slot) → rId`
845
+ * lookup (typically backed by {@link flattenEmbeddedFaces}). Returns `''` when no
846
+ * font has any face with an allocated rId, so callers can emit nothing. Assumes
847
+ * the enclosing document declares the `p:` and `r:` prefixes (presentation.xml
848
+ * does), so no namespace declarations are emitted here.
849
+ */
850
+ function serializeEmbeddedFontLst(fonts, rIdForFace) {
851
+ const entries = [];
852
+ fonts.forEach((font, fontIndex) => {
853
+ const faceXml = [];
854
+ for (const slot of EMBEDDED_FONT_SLOTS) {
855
+ const rId = rIdForFace(fontIndex, slot);
856
+ if (rId === void 0) continue;
857
+ faceXml.push(`<p:${slot} r:id="rId${rId}"/>`);
858
+ }
859
+ if (faceXml.length === 0) return;
860
+ let fontAttrs = `typeface="${encodeXmlAttrValue(font.typeface)}"`;
861
+ if (font.panose !== void 0) fontAttrs += ` panose="${encodeXmlAttrValue(font.panose)}"`;
862
+ if (font.pitchFamily !== void 0) fontAttrs += ` pitchFamily="${font.pitchFamily}"`;
863
+ if (font.charset !== void 0) fontAttrs += ` charset="${font.charset}"`;
864
+ entries.push(`<p:embeddedFont><p:font ${fontAttrs}/>${faceXml.join("")}</p:embeddedFont>`);
865
+ });
866
+ if (entries.length === 0) return "";
867
+ return `<p:embeddedFontLst>${entries.join("")}</p:embeddedFontLst>`;
868
+ }
869
+ // src/gen/drawingml/effect.ts
870
+ /**
871
+ * pptx-ts: DrawingML effects (glow and shadow)
872
+ *
873
+ * Emit the `<a:glow>` / `<a:outerShdw>` / `<a:innerShdw>` children of an
874
+ * `<a:effectLst>`, and normalize the user-facing `shadow` options into the
875
+ * internal shape every emit site reads.
876
+ * @see http://officeopenxml.com/drwSp-effects.php
877
+ */
878
+ /**
879
+ * Creates `a:glow` element
880
+ * @param {TextGlowProps} options glow properties
881
+ * @param {TextGlowProps} defaults defaults for unspecified properties in `opts`
882
+ * @see http://officeopenxml.com/drwSp-effects.php
883
+ * { size: 8, color: 'FFFFFF', opacity: 0.75 };
884
+ */
885
+ function createGlowElement(options, defaults) {
886
+ const opts = {
887
+ ...defaults,
888
+ ...options
889
+ };
890
+ const size = positiveCoordinateEmu(opts.size, "glow/size-out-of-range", "glow.size");
891
+ const color = namedColorOr(opts.color, DEF_FONT_COLOR, "glow.color");
892
+ const opacity = opacityToAlpha(opts.opacity ?? 0);
893
+ return el("a:glow", { rad: size }, raw(createColorElement(color, alphaEl(opacity))));
894
+ }
895
+ /**
896
+ * Creates an `a:outerShdw`/`a:innerShdw` element for a text run, shape, image, or chart.
897
+ * Returns the shadow element only (no wrapping `a:effectLst`) so callers can either combine
898
+ * it with other effects (e.g. glow) inside one `a:effectLst`, or wrap a lone shadow via
899
+ * {@link createShadowEffectLst}.
900
+ *
901
+ * Colors go through {@link createColorElement}, so scheme colors (e.g. `accent1`) are honored;
902
+ * earlier per-site copies hardcoded `a:srgbClr` and silently emitted invalid OOXML for them.
903
+ * @param {ShadowPropsInternal} options shadow properties
904
+ * @param {ShadowPropsInternal} defaults defaults for unspecified properties in `options`
905
+ * @see http://officeopenxml.com/drwSp-effects.php
906
+ * @returns {string} XML string, or '' when type is 'none'
907
+ */
908
+ function createShadowElement(options, defaults) {
909
+ const opts = {
910
+ ...defaults,
911
+ ...options
912
+ };
913
+ if (opts.type === "none") return "";
914
+ const type = opts.type || "outer";
915
+ const blur = positiveCoordinateEmu(opts.blur ?? 0, "shadow/blur-out-of-range", "shadow.blur");
916
+ const offset = positiveCoordinateEmu(opts.offset ?? 0, "shadow/offset-out-of-range", "shadow.offset");
917
+ const angle = convertAngleUnits(opts.angle ?? 0, "shadow.angle");
918
+ const opacity = Math.round((opts._alpha ?? .75) * PERCENT_SCALE);
919
+ const color = namedColorOr(opts.color, DEF_FONT_COLOR, "shadow.color");
920
+ const attrs = {
921
+ ...type === "outer" ? {
922
+ sx: 1e5,
923
+ sy: 1e5,
924
+ kx: 0,
925
+ ky: 0,
926
+ algn: "bl",
927
+ rotWithShape: xsdBool(opts.rotateWithShape)
928
+ } : {},
929
+ blurRad: blur,
930
+ dist: offset,
931
+ dir: angle
932
+ };
933
+ return el(`a:${type}Shdw`, attrs, raw(createColorElement(color, alphaEl(opacity))));
934
+ }
935
+ /**
936
+ * Wraps a lone shadow in the `a:effectLst` that CT_ShapeProperties (shapes/images) and chart
937
+ * marker/data-point properties require. Returns a self-closing `<a:effectLst/>` when there is
938
+ * no shadow (missing/non-object options, or `type: 'none'`), matching the "no effects" element
939
+ * PowerPoint emits. Use this instead of {@link createShadowElement} when the shadow is the only
940
+ * effect; use `createShadowElement` directly when combining with other effects (e.g. glow).
941
+ * @param {ShadowPropsInternal} options shadow properties
942
+ * @param {ShadowPropsInternal} defaults defaults for unspecified properties in `options`
943
+ * @returns {string} `<a:effectLst>…</a:effectLst>` or `<a:effectLst/>`
944
+ */
945
+ function createShadowEffectLst(options, defaults) {
946
+ if (!options || typeof options !== "object") return voidEl("a:effectLst");
947
+ return elOrVoid("a:effectLst", null, createShadowElement(options, defaults));
948
+ }
949
+ /**
950
+ * Normalize a caller's `shadow` into the internal shape every emit site reads: a legal `type`, a
951
+ * whole-degree `angle` inside `ST_PositiveFixedAngle`, a `_alpha` derived from `transparency` or
952
+ * from an RGBA colour's alpha byte, and a colour with no `#` and no alpha byte.
953
+ *
954
+ * **Pure.** It used to normalize the caller's object *in place* and return it, and its four call
955
+ * sites split on which of the two contracts they used: two assigned the result, two discarded it
956
+ * and relied on the mutation. That made the obvious cleanup a trap: making it pure without
957
+ * converting every caller silently drops `transparency` and RGBA alpha on every text run, because
958
+ * `_alpha` is what `genXmlTextRunProperties` reads and nothing else writes it. The value now
959
+ * reaches the emitter as a value rather than as a side effect, and a shadow literal shared across
960
+ * two shapes comes back untouched.
961
+ * @param shadow - the caller's shadow options
962
+ * @returns a fresh normalized bag, or `undefined` when the caller stated no shadow object
963
+ */
964
+ function normalizeShadowOptions(shadow) {
965
+ if (!shadow || typeof shadow !== "object") return void 0;
966
+ const corrected = { ...shadow };
967
+ if (corrected.type !== "outer" && corrected.type !== "inner" && corrected.type !== "none") {
968
+ warn("shadow/invalid-type", "shadow.type options are `outer`, `inner` or `none`.");
969
+ corrected.type = "outer";
970
+ }
971
+ if (corrected.angle !== void 0) corrected.angle = Math.round(clampRangedInput(corrected.angle, 0, 359, "shadow/angle-out-of-range", "shadow.angle", "shadow/angle-non-finite"));
972
+ if (corrected.transparency !== void 0) corrected._alpha = 1 - clampRangedInput(corrected.transparency, 0, 100, "shadow/transparency-out-of-range", "shadow.transparency") / 100;
973
+ if (corrected.color) {
974
+ if (corrected.color.startsWith("#")) {
975
+ warn("shadow/color-has-hash", "shadow.color should not include hash (#) character, , e.g. \"FF0000\"");
976
+ corrected.color = stripHash(corrected.color);
977
+ }
978
+ const rgba = splitRgbaHex(corrected.color);
979
+ if (rgba.alpha !== void 0) {
980
+ if (corrected._alpha === void 0) corrected._alpha = rgba.alpha;
981
+ corrected.color = rgba.rgb;
982
+ }
983
+ }
984
+ return corrected;
985
+ }
986
+ // src/gen/drawingml/line.ts
994
987
  /**
995
988
  * Every key `BorderProps` defines.
996
989
  *
@@ -1090,7 +1083,7 @@ function lineEndEl(tag, value, label) {
1090
1083
  *
1091
1084
  * TypeScript's excess-property check already rejects a stray key on a border written
1092
1085
  * *inline* at the call site. It deliberately does not fire when the border is built as
1093
- * a variable first (`const b = {...}; addTable(rows, { border: b })`) — a variable may
1086
+ * a variable first (`const b = {...}; addTable(rows, { border: b })`), since a variable may
1094
1087
  * legitimately be a supertype. That exemption covers precisely the reuse pattern a
1095
1088
  * shared grid style encourages, so the mistake reaches here typed as valid and the
1096
1089
  * wrong-named value is discarded during generation. A `.pptx` gives no other signal:
@@ -1100,7 +1093,7 @@ function lineEndEl(tag, value, label) {
1100
1093
  function warnUnknownBorderKeys(border) {
1101
1094
  for (const key in border) {
1102
1095
  if (!Object.hasOwn(border, key) || BORDER_KEYS.includes(key)) continue;
1103
- warnOnce("border/unknown-key", `border: unknown option \`${key}\` is ignored — valid keys are ${BORDER_KEYS.join(", ")}. Line thickness is \`width\`, in points.`, {
1096
+ warnOnce("border/unknown-key", `border: unknown option \`${key}\` is ignored. Valid keys are ${BORDER_KEYS.join(", ")}. Line thickness is \`width\`, in points.`, {
1104
1097
  received: key,
1105
1098
  valid: BORDER_KEYS
1106
1099
  });
@@ -1114,8 +1107,8 @@ function warnUnknownBorderKeys(border) {
1114
1107
  * than taking the default: `lineWidthToEmu` refuses `NaN` and clamps `Infinity` to the top of
1115
1108
  * `ST_LineWidth`, as it does for every other line width.
1116
1109
  *
1117
- * Every `BorderProps` the library reads — table cell borders, table-style regions, and
1118
- * every chart border — resolves its width through here, so this doubles as the one
1110
+ * Every `BorderProps` the library reads (table cell borders, table-style regions, and
1111
+ * every chart border) resolves its width through here, so this doubles as the one
1119
1112
  * place that can vet a border's shape for all of them ({@link warnUnknownBorderKeys}).
1120
1113
  * @param {BorderProps} border - border properties (may carry `width`)
1121
1114
  * @param {number} defaultPt - width to use when `width` is not a number
@@ -1128,7 +1121,7 @@ function resolveBorderWidth(border, defaultPt) {
1128
1121
  }
1129
1122
  /**
1130
1123
  * A shape outline with its defaults applied: the kind `resolveLineKind` answers, no transparency, a
1131
- * 1pt width, a solid dash, and on a solid stroke the default colour.
1124
+ * 1pt width, a solid dash, and on a solid stroke the default colour. An inherited line takes none of them.
1132
1125
  *
1133
1126
  * Shapes, line-shaped text boxes and connectors each merged these themselves, and the connector
1134
1127
  * disagreed about zero: `line.width: 0` took the 1pt default on a shape and a text box, and wrote no
@@ -1146,6 +1139,10 @@ function resolveBorderWidth(border, defaultPt) {
1146
1139
  */
1147
1140
  function withLineDefaults(line, colorLabel) {
1148
1141
  const kind = resolveLineKind(line);
1142
+ if (kind === "inherit") return {
1143
+ ...line,
1144
+ type: kind
1145
+ };
1149
1146
  const resolved = {
1150
1147
  ...line,
1151
1148
  type: kind,
@@ -1172,7 +1169,7 @@ function createLineCap(lineCap) {
1172
1169
  *
1173
1170
  * `<a:ln>` takes almost the same fill group as a shape interior, so a stroke can be a gradient
1174
1171
  * or a pattern as well as a solid color. Which one is asked for is {@link resolveLineKind}'s
1175
- * answer, shared with the shape interior — the stroke side used to infer `gradient` from its
1172
+ * answer, shared with the shape interior. The stroke side used to infer `gradient` from its
1176
1173
  * sub-object while the interior inferred nothing, so the same `{ gradient }` spelling painted a
1177
1174
  * gradient outline and a black fill. "Almost" is the picture fill: `EG_LineFillProperties` has
1178
1175
  * no `a:blipFill`, and `resolveLineKind` is where that is refused.
@@ -1196,20 +1193,20 @@ function genXmlLineFill(line) {
1196
1193
  * Four builders used to turn a `BorderProps` into a line element, each with its own
1197
1194
  * defaults, and the differences between them were only visible by reading all four: a
1198
1195
  * chart data point's outline, a chart-area or plot-area border, the `<c:spPr>` line the
1199
- * plot and chart space share, and a table cell's six edges. They agree on the shape —
1200
- * `w` and `cap` on the element, a solid paint from `color`/`transparency`, then an
1201
- * optional dash and whatever the schema allows after it — and disagree only on what fills
1196
+ * plot and chart space share, and a table cell's six edges. They agree on the shape
1197
+ * (`w` and `cap` on the element, a solid paint from `color`/`transparency`, then an
1198
+ * optional dash and whatever the schema allows after it) and disagree only on what fills
1202
1199
  * those slots. Those are this function's parameters, so each caller is now a named
1203
1200
  * argument set rather than a re-derivation.
1204
1201
  *
1205
1202
  * Two things are deliberately *not* parameters. A stroke that paints nothing is spelled
1206
1203
  * differently by each caller (a bare `<a:ln><a:noFill/></a:ln>` for a chart, an
1207
1204
  * attribute-carrying `w="0"` rule for a table cell, and nothing at all for a data point,
1208
- * whose caller decides whether to emit it) — see {@link noStrokeLine} for the shared
1205
+ * whose caller decides whether to emit it); see {@link noStrokeLine} for the shared
1209
1206
  * spelling of the first. And the colour default is taken on `||`, not `??`, so an empty
1210
1207
  * `color` reads as *unstated* rather than reaching the colour validator as a bad value:
1211
1208
  * three of the four callers used to hand `''` straight through, which warns
1212
- * `color/invalid-value` and paints `DEF_FONT_COLOR` — black — instead of the border
1209
+ * `color/invalid-value` and paints `DEF_FONT_COLOR` (black) instead of the border
1213
1210
  * default they had named a line earlier. The chart-border builder already read it this way,
1214
1211
  * and `dataBorder` is the only one of the other three a caller can actually reach with an
1215
1212
  * empty string; the rest resolve their colour in the definition step first.
@@ -1238,7 +1235,7 @@ function borderLine(name, border, spec) {
1238
1235
  * The `<a:ln>` that paints no stroke at all: an explicit `<a:noFill/>` on a bare element.
1239
1236
  *
1240
1237
  * This is the chart side's spelling of "no line" and is a different statement from a
1241
- * {@link borderLine} whose paint is absent — an omitted `a:ln` inherits the theme's
1238
+ * {@link borderLine} whose paint is absent: an omitted `a:ln` inherits the theme's
1242
1239
  * `a:lnRef`, where this overrides it. A table cell spells the same intent with the
1243
1240
  * attributes still on the element and `w="0"`, which is why that one is not this.
1244
1241
  * @returns the `<a:ln><a:noFill/></a:ln>` element XML
@@ -1246,13 +1243,12 @@ function borderLine(name, border, spec) {
1246
1243
  function noStrokeLine() {
1247
1244
  return el("a:ln", null, raw(voidEl("a:noFill")));
1248
1245
  }
1249
- //#endregion
1250
- //#region src/gen/define/hyperlinks.ts
1246
+ // src/gen/define/hyperlinks.ts
1251
1247
  /**
1252
1248
  * The relationship record one hyperlink serializes to.
1253
1249
  *
1254
- * `data` distinguishes the two targets a hyperlink rel can have — `'slide'` for an internal
1255
- * slide-to-slide link, `'dummy'` for an external URL — and `Target` is stored RAW, because
1250
+ * `data` distinguishes the two targets a hyperlink rel can have (`'slide'` for an internal
1251
+ * slide-to-slide link, `'dummy'` for an external URL), and `Target` is stored RAW, because
1256
1252
  * every emitter escapes it on the way out (see the note on `SlideRel.Target`).
1257
1253
  *
1258
1254
  * Four sites built this record by hand: the two below, `addImage`'s own hyperlink, and the
@@ -1276,7 +1272,8 @@ function hyperlinkRel(rId, hyperlink) {
1276
1272
  * definer and the run emitter each checked their own subset, and the shape path checked nothing. An
1277
1273
  * image refused an action-only link that a shape accepted, and nothing refused `url` together with
1278
1274
  * `slide`: that wrote two `<a:hlinkClick>` into one `<p:cNvPr>`, which allows one, over a slide-typed
1279
- * relationship whose target was built from the URL.
1275
+ * relationship whose target was built from the URL. An `action` beside a `url` or a `slide` was
1276
+ * dropped without a word, so it is refused too.
1280
1277
  *
1281
1278
  * A `slide` has to be a 1-based slide number. `-1` and `1.5` were written as the targets
1282
1279
  * `slide-1.xml` and `slide1.5.xml`, parts no deck has. Whether the numbered slide exists is only
@@ -1288,17 +1285,23 @@ function hyperlinkRel(rId, hyperlink) {
1288
1285
  function validateHyperlink(hyperlink, call) {
1289
1286
  if (typeof hyperlink !== "object" || hyperlink === null) throw new InvalidOptionError("hyperlink/not-an-object", `${call}: \`hyperlink\` option should be an object. Ex: \`hyperlink:{url:'https://github.com'}\``);
1290
1287
  const { url, slide, action } = hyperlink;
1291
- if (url && slide) throw new InvalidOptionError("hyperlink/conflicting-targets", `${call}: \`hyperlink\` takes one destination but states both \`url\` and \`slide\`.`);
1288
+ const stated = [
1289
+ "url",
1290
+ "slide",
1291
+ "action"
1292
+ ].filter((key) => hyperlink[key]);
1293
+ if (stated.length > 1) throw new InvalidOptionError("hyperlink/conflicting-targets", `${call}: \`hyperlink\` takes one destination but states ${stated.map((key) => `\`${key}\``).join(" and ")}.`);
1292
1294
  if (slide != null && !(Number.isInteger(slide) && slide >= 1)) throw new InvalidOptionError("hyperlink/invalid-slide", `${call}: \`hyperlink.slide\` is a 1-based slide number, got ${typeof slide === "string" ? JSON.stringify(slide) : String(slide)}.`);
1293
1295
  if (!url && !slide && !action) throw new InvalidOptionError("hyperlink/missing-target", `${call}: \`hyperlink\` requires either \`url\`, \`slide\`, or \`action\``);
1296
+ if (action && !SHOW_JUMPS.includes(action)) throw new InvalidOptionError("hyperlink/invalid-action", `${call}: \`hyperlink.action\` is one of ${SHOW_JUMPS.join(", ")}, got ${JSON.stringify(action)}.`);
1294
1297
  }
1295
1298
  /**
1296
1299
  * Mint a fresh rel id for `hyperlink`, register it on `target`, and stamp the id back onto
1297
1300
  * the hyperlink so the emitter can write `r:id`.
1298
1301
  *
1299
1302
  * The id comes from {@link getNewRelId}, which skips every id already held on the slide.
1300
- * `addImage` used to increment the image's own id instead, which is how an SVG picture — a
1301
- * pair that already consumes two ids — ended up sharing the second of them with its
1303
+ * `addImage` used to increment the image's own id instead, which is how an SVG picture (a
1304
+ * pair that already consumes two ids) ended up sharing the second of them with its
1302
1305
  * hyperlink and emitting a duplicate `Relationship Id`.
1303
1306
  * @returns the allocated relationship id
1304
1307
  */
@@ -1310,9 +1313,7 @@ function registerHyperlinkRel(target, hyperlink) {
1310
1313
  }
1311
1314
  /** The objects one level of a text or table-cell tree holds: a table row is itself an array. */
1312
1315
  function hyperlinkTreeLevel(text) {
1313
- if (typeof text === "string" || typeof text === "number") return [];
1314
- if (Array.isArray(text)) return text;
1315
- return typeof text === "object" ? [text] : [];
1316
+ return Array.isArray(text) ? text : [text];
1316
1317
  }
1317
1318
  /**
1318
1319
  * Parses text/text-objects from `addText()` and `addTable()` methods; creates 'hyperlink'-type Slide Rels for each hyperlink found
@@ -1321,7 +1322,7 @@ function hyperlinkTreeLevel(text) {
1321
1322
  * refused link on a second run used to throw after the first run's relationship was already on
1322
1323
  * the slide, left pointing at a shape that was never added.
1323
1324
  * @param {PresSlideInternal} target - slide object that any hyperlinks will be be added to
1324
- * @param {number | string | TextProps | TextProps[] | TableCellInternal[][]} text - text to parse
1325
+ * @param {SlideObject | TextProps | TextProps[] | TableCellInternal[][]} text - text to parse
1325
1326
  * @param call - the method the objects were authored through, for a refused hyperlink's message
1326
1327
  */
1327
1328
  function createHyperlinkRels(target, text, call = "addText") {
@@ -1366,8 +1367,7 @@ function registerHyperlinkTree(target, tree) {
1366
1367
  }
1367
1368
  });
1368
1369
  }
1369
- //#endregion
1370
- //#region src/gen/drawingml/bullet.ts
1370
+ // src/gen/drawingml/bullet.ts
1371
1371
  /** A bullet `characterCode`: four hex digits naming a code point. */
1372
1372
  const CHARACTER_CODE = /^[0-9A-Fa-f]{4}$/;
1373
1373
  /** `ST_TextBulletStartAtNum`'s range. */
@@ -1412,13 +1412,12 @@ function buAutoNumEl(type, startAt, label) {
1412
1412
  startAt: startAt === void 0 ? null : clampRangedInput(Math.round(startAt), START_AT_MIN, START_AT_MAX, "bullet/start-at-out-of-range", `${label} \`numberStartAt\``, "bullet/start-at-not-a-number")
1413
1413
  });
1414
1414
  }
1415
- //#endregion
1416
- //#region src/gen/drawingml/clamp.ts
1415
+ // src/gen/drawingml/clamp.ts
1417
1416
  /**
1418
1417
  * pptx-ts: DrawingML value clamps
1419
1418
  *
1420
1419
  * Clamp font/character/line spacing values into their ST_* schema ranges and convert to the
1421
- * units the attributes expect — hundredths of a point for most of them, thousandths of a
1420
+ * units the attributes expect: hundredths of a point for most of them, thousandths of a
1422
1421
  * percent for the one measure the caller states as a multiple rather than a size.
1423
1422
  * Out-of-range values make PowerPoint report the package as needing repair.
1424
1423
  */
@@ -1462,6 +1461,16 @@ const CHAR_SPACING = {
1462
1461
  min: -4e5,
1463
1462
  max: 4e5
1464
1463
  };
1464
+ const KERNING = {
1465
+ code: "text/kerning-out-of-range",
1466
+ nonFiniteCode: "coord/non-finite",
1467
+ label: "kerning",
1468
+ range: "0-4000pt",
1469
+ convert: ptToHundredths,
1470
+ perUnit: 100,
1471
+ min: 0,
1472
+ max: 4e5
1473
+ };
1465
1474
  const PARA_MARGIN = {
1466
1475
  code: "text/paragraph-margin-out-of-range",
1467
1476
  nonFiniteCode: "coord/non-finite",
@@ -1535,9 +1544,13 @@ function clampFontSizeSz(fontSizePts, label) {
1535
1544
  function clampCharSpacingSpc(charSpacingPts) {
1536
1545
  return clampWithWarn(charSpacingPts, CHAR_SPACING);
1537
1546
  }
1547
+ /** Clamp a kerning threshold (points) into ST_TextNonNegativePoint (0..4000pt); returns hundredths for the `kern` attribute. */
1548
+ function clampKerningKern(kerningPts) {
1549
+ return clampWithWarn(kerningPts, KERNING);
1550
+ }
1538
1551
  /**
1539
1552
  * Clamp a paragraph's left margin (points) into ST_TextMargin (0..4032pt, i.e. 0..51206400 EMU);
1540
- * returns EMU for `a:pPr/@marL`. The type is unsigned — a negative margin is not a narrower one,
1553
+ * returns EMU for `a:pPr/@marL`. The type is unsigned. A negative margin is not a narrower one;
1541
1554
  * it is a value PowerPoint reports as needing repair.
1542
1555
  */
1543
1556
  function clampParaMarginEmu(marginPts) {
@@ -1587,13 +1600,12 @@ function clampLineSpacingPts(lineSpacingPts) {
1587
1600
  function clampLineSpacingMultiplePct(multiple) {
1588
1601
  return clampWithWarn(multiple, LINE_SPACING_MULTIPLE);
1589
1602
  }
1590
- //#endregion
1591
- //#region src/gen/drawingml/math.ts
1603
+ // src/gen/drawingml/math.ts
1592
1604
  /**
1593
1605
  * pptx-ts: DrawingML native equations (OMML)
1594
1606
  *
1595
1607
  * Wrap raw OMML markup into the `<a14:m>` marker PowerPoint uses for editable
1596
- * equations — as a standalone display paragraph (`genXmlMathParagraph`) or as an
1608
+ * equations, either as a standalone display paragraph (`genXmlMathParagraph`) or as an
1597
1609
  * inline run that flows between the surrounding `<a:r>` runs (`genXmlInlineMath`).
1598
1610
  */
1599
1611
  /** The `<a14:m>` marker, carrying both namespace declarations so the supplied OMML needs none. */
@@ -1616,7 +1628,7 @@ function a14Math(mathXml) {
1616
1628
  * @returns {string} an `<a:p>` math paragraph
1617
1629
  */
1618
1630
  function genXmlMathParagraph(omml) {
1619
- const trimmed = (omml || "").trim();
1631
+ const trimmed = omml.trim();
1620
1632
  const paraPr = el("m:oMathParaPr", null, raw(voidEl("m:jc", { "m:val": "centerGroup" })));
1621
1633
  return el("a:p", null, [a14Math(trimmed.startsWith("<m:oMathPara") ? trimmed : trimmed.includes("<m:oMath") ? el("m:oMathPara", null, [paraPr, trimmed].map(raw)) : el("m:oMathPara", null, [paraPr, el("m:oMath", null, raw(trimmed))].map(raw))), voidEl("a:endParaRPr", { lang: "en-US" })].map(raw));
1622
1634
  }
@@ -1625,7 +1637,7 @@ function genXmlMathParagraph(omml) {
1625
1637
  *
1626
1638
  * Unlike the display form ({@link genXmlMathParagraph}), an inline equation is *not* its own
1627
1639
  * paragraph: PowerPoint authors an `<a14:m>` marker wrapping a bare `<m:oMath>` (no `<m:oMathPara>`,
1628
- * no `<m:oMathParaPr>`/`<m:jc>`) that flows between the surrounding `<a:r>` runs in one `<a:p>` —
1640
+ * no `<m:oMathParaPr>`/`<m:jc>`) that flows between the surrounding `<a:r>` runs in one `<a:p>`,
1629
1641
  * pinned by the `math-omml-inline.pptx` oracle. The `mc:AlternateContent` envelope stays at the
1630
1642
  * shape level (see `objectHasMath`), so nothing wraps the run itself. We declare the `a14` and `m`
1631
1643
  * namespaces on the `<a14:m>` element so the supplied OMML needs none of its own, and accept the
@@ -1636,12 +1648,11 @@ function genXmlMathParagraph(omml) {
1636
1648
  * @returns {string} an `<a14:m>` inline equation run
1637
1649
  */
1638
1650
  function genXmlInlineMath(omml) {
1639
- const trimmed = (omml || "").trim();
1651
+ const trimmed = omml.trim();
1640
1652
  const oMathMatch = trimmed.match(/<m:oMath[\s>][\s\S]*<\/m:oMath>/);
1641
1653
  return a14Math(oMathMatch ? oMathMatch[0] : el("m:oMath", null, raw(trimmed)));
1642
1654
  }
1643
- //#endregion
1644
- //#region src/gen/drawingml/text-run.ts
1655
+ // src/gen/drawingml/text-run.ts
1645
1656
  /**
1646
1657
  * `<a:extLst>` marking a hyperlink whose color should follow the text color rather than the
1647
1658
  * theme's hyperlink color. The leading spaces are byte-significant: this block was authored as
@@ -1649,7 +1660,7 @@ function genXmlInlineMath(omml) {
1649
1660
  * with `openPrefix`/`closePrefix` rather than being tidied away.
1650
1661
  */
1651
1662
  const HLINK_TEXT_COLOR_EXT = el("a:extLst", null, raw(el("a:ext", { uri: "{A12FA001-AC4F-418D-AE19-62706E023703}" }, raw(voidEl("ahyp:hlinkClr", {
1652
- "xmlns:ahyp": "http://schemas.microsoft.com/office/drawing/2018/hyperlinkcolor",
1663
+ "xmlns:ahyp": OOXML_NS.ahyp,
1653
1664
  val: "tx"
1654
1665
  }, { openPrefix: " " })), {
1655
1666
  openPrefix: " ",
@@ -1663,7 +1674,7 @@ const HLINK_TEXT_COLOR_EXT = el("a:extLst", null, raw(el("a:ext", { uri: "{A12FA
1663
1674
  *
1664
1675
  * Two call sites downstream of the emitter act on "this paragraph has a bullet" rather than on
1665
1676
  * "this paragraph says something about its bullet", and both were plain truthiness tests until
1666
- * `'inherit'` — a truthy string that draws nothing — joined the union. Left as truthiness they
1677
+ * `'inherit'`, a truthy string that draws nothing, joined the union. Left as truthiness they
1667
1678
  * would have broken the line grouping (every run starting its own paragraph) and eaten a real
1668
1679
  * leading `•` from text that emits no `a:buChar` to duplicate it.
1669
1680
  * @param {TextPropsOptions['bullet']} bullet - the option value
@@ -1677,12 +1688,12 @@ function emitsBulletMarkup(bullet) {
1677
1688
  *
1678
1689
  * Three inputs, because the option has three states and so does the attribute: an explicit
1679
1690
  * number is clamped and written, `'inherit'` writes nothing, and an absent option falls back to
1680
- * whatever the `bullet` arm decided — which is also `null` for `bullet: 'inherit'`, the one arm
1691
+ * whatever the `bullet` arm decided, which is also `null` for `bullet: 'inherit'`, the one arm
1681
1692
  * that states nothing on its own.
1682
1693
  *
1683
1694
  * BOTH arms go through the clamp. The fallback used to be returned verbatim on the grounds that
1684
1695
  * the emitter computed it, but the emitter computes it from the caller's `bullet.indent` and
1685
- * `indentLevel`, so it is caller input one multiplication later — and it reached `@marL` at
1696
+ * `indentLevel`, so it is caller input one multiplication later, and it reached `@marL` at
1686
1697
  * ~6700x the `ST_TextMargin` ceiling. The fallback arrives in EMU rather than points because the
1687
1698
  * bullet arms do their arithmetic there; the round trip through the clamp's own unit is exact
1688
1699
  * for any value in range, so a paragraph that was already legal emits the same bytes.
@@ -1700,7 +1711,7 @@ function resolveParagraphMargin(option, fallbackEmu, clamp) {
1700
1711
  * The paragraph's outline level, or `0` when the caller named none or named one `a:p/@lvl`
1701
1712
  * cannot hold.
1702
1713
  *
1703
- * Resolved once because the level is read twice — for the attribute, and as the multiplier on
1714
+ * Resolved once because the level is read twice: for the attribute, and as the multiplier on
1704
1715
  * the bullet arms' default `@marL`. Validating it for one use and multiplying a margin by it in
1705
1716
  * the other is how `indentLevel: 1e6` reached `marL="342900342900"` under a warning that said
1706
1717
  * the value was being ignored.
@@ -1715,8 +1726,8 @@ function resolveIndentLevel(level) {
1715
1726
  }
1716
1727
  /**
1717
1728
  * The `@marL` a bulleted paragraph defaults to: the bullet's own hang, plus one more of it for
1718
- * every outline level. Two arms of the bullet chain write it — `bullet: {…}` and `bullet: true`
1719
- * — and each had the expression spelled out, which is how one of them would have been fixed
1729
+ * every outline level. Two arms of the bullet chain write it (`bullet: {…}` and `bullet: true`),
1730
+ * and each had the expression spelled out, which is how one of them would have been fixed
1720
1731
  * and the other left behind.
1721
1732
  * @param {number} bulletMarLEmu - the hang one level is worth, in EMU
1722
1733
  * @param {number} indentLevel - the resolved outline level
@@ -1759,7 +1770,7 @@ function genXmlParagraphProperties(textObj, isDefault) {
1759
1770
  const bullet = opts.bullet;
1760
1771
  const bulletImage = bullet.image;
1761
1772
  const isPictureBullet = !!(bulletImage && (bulletImage.path || bulletImage.data));
1762
- if (opts.bullet?.indent) bulletMarL = ptsToEmuLenient(opts.bullet.indent);
1773
+ if (opts.bullet.indent) bulletMarL = ptsToEmuLenient(opts.bullet.indent);
1763
1774
  defaultMarL = bulletHangMarL(bulletMarL, indentLevel);
1764
1775
  defaultIndent = -bulletMarL;
1765
1776
  rejectEmptyColor(opts.bullet.color, "bullet.color");
@@ -1784,7 +1795,7 @@ function genXmlParagraphProperties(textObj, isDefault) {
1784
1795
  defaultMarL = bulletHangMarL(bulletMarL, indentLevel);
1785
1796
  defaultIndent = -bulletMarL;
1786
1797
  strXmlBullet = buCharEl(void 0, "&#x2022;", "bullet");
1787
- } else if (!opts.bullet) {
1798
+ } else {
1788
1799
  defaultMarL = 0;
1789
1800
  defaultIndent = 0;
1790
1801
  indentBeforeMarL = true;
@@ -1806,6 +1817,17 @@ function genXmlParagraphProperties(textObj, isDefault) {
1806
1817
  return paragraphPropXml;
1807
1818
  }
1808
1819
  /**
1820
+ * `underline` in its object form. `true` is shorthand for a single line; `false` states nothing, as
1821
+ * omitting the option does. Every `a:rPr` writer reads it through here, so the shorthand holds for a
1822
+ * table cell and a slide number as it does for a text run.
1823
+ * @param underline - the option as the caller wrote it
1824
+ * @returns the object form, or `undefined` when it states nothing
1825
+ */
1826
+ function underlineProps(underline) {
1827
+ if (underline === true) return { style: "sng" };
1828
+ return typeof underline === "object" && underline !== null ? underline : void 0;
1829
+ }
1830
+ /**
1809
1831
  * Generate XML Text Run Properties (`a:rPr`)
1810
1832
  * @param {ObjectOptions|TextPropsOptions} opts - text options
1811
1833
  * @param {boolean} isDefault - whether these are the default text run properties
@@ -1814,32 +1836,33 @@ function genXmlParagraphProperties(textObj, isDefault) {
1814
1836
  function genXmlTextRunProperties(opts, isDefault) {
1815
1837
  let runProps = "";
1816
1838
  const runPropsTag = isDefault ? "a:defRPr" : "a:rPr";
1817
- const underline = typeof opts.underline === "object" && opts.underline?.style ? checkEnumOrWarn(opts.underline.style, TEXT_UNDERLINE_TYPES, "text/invalid-underline", "text `underline.style`") : opts.hyperlink ? "sng" : null;
1818
- const baseline = opts.baseline ? Math.round(opts.baseline * 50) : opts.subscript ? -4e4 : opts.superscript ? 3e4 : null;
1839
+ const underlineOpts = underlineProps(opts.underline);
1840
+ const underline = underlineOpts?.style ? checkEnumOrWarn(underlineOpts.style, TEXT_UNDERLINE_TYPES, "text/invalid-underline", "text `underline.style`") : opts.hyperlink ? "sng" : null;
1841
+ const baseline = opts.baseline ? Math.round(opts.baseline * 1e3) : opts.subscript ? -4e4 : opts.superscript ? 3e4 : null;
1819
1842
  const attrs = {
1820
1843
  lang: opts.lang ? opts.lang : "en-US",
1821
1844
  altLang: opts.lang ? "en-US" : null,
1822
1845
  sz: mapStated(opts.fontSize, clampFontSizeSz) ?? null,
1823
- b: xsdBoolIfTrue(opts?.bold),
1824
- i: xsdBoolIfTrue(opts?.italic),
1825
- strike: opts?.strike ? typeof opts.strike === "string" ? checkEnumOrWarn(opts.strike, TEXT_STRIKE_TYPES, "text/invalid-strike", "text `strike`") : "sngStrike" : null,
1826
- cap: opts?.caps ? checkEnumOrWarn(opts.caps, TEXT_CAPS_TYPES, "text/invalid-caps", "text `caps`") : null,
1846
+ b: xsdBoolIfTrue(opts.bold),
1847
+ i: xsdBoolIfTrue(opts.italic),
1848
+ strike: opts.strike ? typeof opts.strike === "string" ? checkEnumOrWarn(opts.strike, TEXT_STRIKE_TYPES, "text/invalid-strike", "text `strike`") : "sngStrike" : null,
1849
+ cap: opts.caps ? checkEnumOrWarn(opts.caps, TEXT_CAPS_TYPES, "text/invalid-caps", "text `caps`") : null,
1827
1850
  u: underline,
1828
1851
  baseline,
1829
1852
  spc: mapStated(opts.charSpacing, clampCharSpacingSpc) ?? null,
1830
- kern: opts.charSpacing ? 0 : null,
1853
+ kern: opts.kerning === 0 ? 0 : mapStated(opts.kerning, clampKerningKern) ?? (opts.charSpacing ? 0 : null),
1831
1854
  dirty: "0"
1832
1855
  };
1833
1856
  const hasShadow = !!opts.shadow && opts.shadow.type !== "none";
1834
- if (opts.color || opts.fontFace || opts.outline || opts.glow || hasShadow || typeof opts.underline === "object" && opts.underline.color) {
1857
+ if (opts.color || opts.fontFace || opts.fontFaceEA || opts.outline || opts.glow || hasShadow || underlineOpts?.color) {
1835
1858
  if (opts.outline && typeof opts.outline === "object") runProps += el("a:ln", { w: lineWidthToEmu(mapStated(opts.outline.size, (size) => size) ?? .75, "outline.size") }, raw(genXmlColorSelection(namedColorOr(opts.outline.color, "FFFFFF", "outline.color"))));
1836
1859
  rejectEmptyColor(opts.color, "color");
1837
1860
  if (opts.color) runProps += genXmlColorSelection(solidPaint(opts.color, opts.transparency));
1838
1861
  if (opts.glow || hasShadow) runProps += el("a:effectLst", null, [opts.glow ? raw(createGlowElement(opts.glow, DEF_TEXT_GLOW)) : null, hasShadow ? raw(createShadowElement(opts.shadow, DEF_TEXT_SHADOW)) : null]);
1839
1862
  rejectEmptyColor(opts.highlight, "highlight");
1840
1863
  if (opts.highlight) runProps += el("a:highlight", null, raw(createColorElement(opts.highlight)));
1841
- if (typeof opts.underline === "object") rejectEmptyColor(opts.underline.color, "underline.color");
1842
- if (typeof opts.underline === "object" && opts.underline.color) runProps += el("a:uFill", null, raw(genXmlColorSelection(opts.underline.color)));
1864
+ rejectEmptyColor(underlineOpts?.color, "underline.color");
1865
+ if (underlineOpts?.color) runProps += el("a:uFill", null, raw(genXmlColorSelection(underlineOpts.color)));
1843
1866
  if (opts.fontFace) {
1844
1867
  runProps += voidEl("a:latin", {
1845
1868
  typeface: opts.fontFace,
@@ -1848,7 +1871,7 @@ function genXmlTextRunProperties(opts, isDefault) {
1848
1871
  });
1849
1872
  if (opts.fontFaceEA) runProps += voidEl("a:ea", { typeface: opts.fontFaceEA });
1850
1873
  runProps += voidEl("a:cs", { typeface: opts.fontFace });
1851
- }
1874
+ } else if (opts.fontFaceEA) runProps += voidEl("a:ea", { typeface: opts.fontFaceEA });
1852
1875
  }
1853
1876
  if (opts.hyperlink) {
1854
1877
  try {
@@ -1856,7 +1879,12 @@ function genXmlTextRunProperties(opts, isDefault) {
1856
1879
  } catch (err) {
1857
1880
  throw new InternalError("hyperlink/not-validated", `A run reached the emitter with a hyperlink its definer should have refused: ${err.message}`);
1858
1881
  }
1859
- if (opts.hyperlink.url || opts.hyperlink.slide) {
1882
+ if (opts.hyperlink.action && !opts.hyperlink.url && !opts.hyperlink.slide && !opts._shapeHyperlink) runProps += voidEl("a:hlinkClick", {
1883
+ "r:id": "",
1884
+ action: `ppaction://hlinkshowjump?jump=${opts.hyperlink.action}`,
1885
+ tooltip: opts.hyperlink.tooltip ?? ""
1886
+ });
1887
+ else if (opts.hyperlink.url || opts.hyperlink.slide) {
1860
1888
  const link = opts.hyperlink;
1861
1889
  const linkAttrs = opts.hyperlink.url ? {
1862
1890
  "r:id": `rId${link._rId}`,
@@ -1926,7 +1954,7 @@ function groupRunsIntoLines(arrTextObjects, opts) {
1926
1954
  textObj.options.breakLine = false;
1927
1955
  }
1928
1956
  arrTexts.push(textObj);
1929
- if (arrTexts.length > 0 && textObj.options.breakLine) {
1957
+ if (textObj.options.breakLine) {
1930
1958
  if (idx + 1 < arrTextObjects.length) {
1931
1959
  arrLines.push(arrTexts);
1932
1960
  arrTexts = [];
@@ -1937,7 +1965,7 @@ function groupRunsIntoLines(arrTextObjects, opts) {
1937
1965
  return arrLines;
1938
1966
  }
1939
1967
  /**
1940
- * The options a run inherits from its shape — exactly the keys `genXmlTextRunProperties`
1968
+ * The options a run inherits from its shape: exactly the keys `genXmlTextRunProperties`
1941
1969
  * reads off a run's bag, in the order it reads them.
1942
1970
  *
1943
1971
  * This used to be `Object.entries(opts)` filtered on `!textOptions[key]`, which had two
@@ -1963,6 +1991,7 @@ const RUN_INHERITABLE_OPTIONS = [
1963
1991
  "subscript",
1964
1992
  "superscript",
1965
1993
  "charSpacing",
1994
+ "kerning",
1966
1995
  "color",
1967
1996
  "transparency",
1968
1997
  "fontFace",
@@ -2024,8 +2053,9 @@ const PARAGRAPH_FALSY_IS_UNSTATED = [
2024
2053
  /**
2025
2054
  * Render each grouped line to an `<a:p>` paragraph: paragraph props, inherited run options,
2026
2055
  * text runs (and inline/display math), and the closing endParaRPr.
2056
+ * @param slideColor - the colour a run marked `_slideColor` is written in
2027
2057
  */
2028
- function renderTextParagraphsXml(arrLines, slideObj, opts) {
2058
+ function renderTextParagraphsXml(arrLines, slideObj, opts, slideColor) {
2029
2059
  let strSlideXml = "";
2030
2060
  arrLines.forEach((line) => {
2031
2061
  const firstRun = line[0];
@@ -2057,6 +2087,7 @@ function renderTextParagraphsXml(arrLines, slideObj, opts) {
2057
2087
  if (key === "color" && textObj.options.hyperlink) return;
2058
2088
  setOrClear(textOptions, key, textOptions[key] ?? shapeOptions[key]);
2059
2089
  });
2090
+ setOrClear(textOptions, "_shapeHyperlink", slideObj._type !== "tablecell" && textOptions.hyperlink !== void 0 && textOptions.hyperlink === shapeOptions.hyperlink ? true : void 0);
2060
2091
  let _textRunObj = textObj;
2061
2092
  if (idx === 0 && emitsBulletMarkup(line[0]?.options.bullet) && typeof textObj.text === "string") {
2062
2093
  const _stripped = textObj.text.replace(/^[\u2022\u25E6\u25AA\u25AB\u25CF\u25CB\u2023\u2043\u2219]\s*/, "");
@@ -2067,7 +2098,16 @@ function renderTextParagraphsXml(arrLines, slideObj, opts) {
2067
2098
  }
2068
2099
  const isEmptyBreakArtifact = _textRunObj.text === "" && line.length > 1;
2069
2100
  if (_textRunObj.math) paraXml += genXmlInlineMath(_textRunObj.math);
2070
- else if (!isEmptyBreakArtifact) paraXml += genXmlTextRun(_textRunObj);
2101
+ else if (!isEmptyBreakArtifact) {
2102
+ const runOpts = _textRunObj.options;
2103
+ paraXml += genXmlTextRun(runOpts._slideColor && runOpts.color === void 0 ? {
2104
+ ..._textRunObj,
2105
+ options: {
2106
+ ...runOpts,
2107
+ color: slideColor
2108
+ }
2109
+ } : _textRunObj);
2110
+ }
2071
2111
  if (!textObj.text && opts.fontSize || textObj.options.fontSize) {
2072
2112
  reqsClosingFontSize = true;
2073
2113
  closingFontSize = closingFontSize ?? textObj.options.fontSize;
@@ -2097,8 +2137,7 @@ function renderTextParagraphsXml(arrLines, slideObj, opts) {
2097
2137
  });
2098
2138
  return strSlideXml;
2099
2139
  }
2100
- //#endregion
2101
- //#region src/gen/drawingml/geometry.ts
2140
+ // src/gen/drawingml/geometry.ts
2102
2141
  /**
2103
2142
  * pptx-ts: DrawingML geometry
2104
2143
  *
@@ -2107,8 +2146,8 @@ function renderTextParagraphsXml(arrLines, slideObj, opts) {
2107
2146
  * shape and image code paths so path/preset emission stays in one place.
2108
2147
  */
2109
2148
  /**
2110
- * Several `<a:custGeom>` children are emitted as `<a:avLst />` — with a space before the
2111
- * slash — where the rest of the tree writes `<a:avLst/>`. Byte-significant, so it stays.
2149
+ * Several `<a:custGeom>` children are emitted as `<a:avLst />` (with a space before the
2150
+ * slash) where the rest of the tree writes `<a:avLst/>`. Byte-significant, so it stays.
2112
2151
  */
2113
2152
  const SPACE_BEFORE_SLASH = { closePrefix: " " };
2114
2153
  /**
@@ -2122,12 +2161,12 @@ const BEZ_INDENT = {
2122
2161
  };
2123
2162
  /**
2124
2163
  * The 17 guide-formula operations ECMA-376 Part 1 §20.1.9.11 (`a:gd/@fmla`) defines.
2125
- * The set is closed — the spec enumerates every operation with its arity — so an
2164
+ * The set is closed (the spec enumerates every operation with its arity), so an
2126
2165
  * unrecognized leading token is always a caller mistake, not a newer dialect.
2127
2166
  *
2128
2167
  * Only the operation is checked. The operands are deliberately left uninterpreted:
2129
2168
  * they may be literals, adjust names, or other guide names resolved in declaration
2130
- * order, and validating them would mean re-implementing the geometry engine — which
2169
+ * order, and validating them would mean re-implementing the geometry engine, which
2131
2170
  * is exactly what this passthrough exists to avoid.
2132
2171
  */
2133
2172
  const GEOM_GUIDE_OPS = /* @__PURE__ */ new Set([
@@ -2164,8 +2203,8 @@ const RECT_RADIUS_ADJ1_SHAPES = /* @__PURE__ */ new Set(["round2SameRect", "roun
2164
2203
  *
2165
2204
  * An unknown `prst` value corrupts the package: PowerPoint shows the "needs
2166
2205
  * repair" dialog and drops the shape. So this is the safety net for every
2167
- * `prstGeom` emitter — `addShape`, and the `shape` option on `addText`/`addImage`
2168
- * — and it is also called during `addShape` normalization, so the caller sees
2206
+ * `prstGeom` emitter (`addShape`, and the `shape` option on `addText`/`addImage`),
2207
+ * and it is also called during `addShape` normalization, so the caller sees
2169
2208
  * their own typo at the API boundary rather than at serialization time.
2170
2209
  * @param {string} shapeName - the resolved preset geometry name
2171
2210
  * @throws {InvalidOptionError} `shape/unknown-preset` when the name is not a preset
@@ -2253,8 +2292,9 @@ function isArcPoint(point) {
2253
2292
  * `<a:ahLst>`) and guide formulas (`guides` → `<a:gdLst>`) that make the shape connectable and
2254
2293
  * editable. Shared by the shape and image code paths so that path emission stays in one place.
2255
2294
  * Points and connection-site/handle positions are authored in the object's own inch/EMU space
2256
- * (0..cx, 0..cy) — not slide-relative and not normalized; a bare non-`Coord` string is passed
2257
- * through verbatim as a guide-name reference (matching `ST_AdjCoordinate`'s number-or-name union).
2295
+ * (0..cx, 0..cy), not slide-relative and not normalized; a bare non-`Coord` string, in a path
2296
+ * point as in a site or handle, is passed through verbatim as a guide-name reference (matching
2297
+ * `ST_AdjCoordinate`'s number-or-name union).
2258
2298
  * @param {ObjectOptions} options - object options carrying `points`/`guides`/`connectionSites`/`adjustHandles`
2259
2299
  * @param {number} cx - object width (EMU), used as the path viewport width
2260
2300
  * @param {number} cy - object height (EMU), used as the path viewport height
@@ -2273,20 +2313,24 @@ function genXmlCustGeom(options, cx, cy, layout) {
2273
2313
  if (typeof v === "string" && /^[A-Za-z_]/.test(v.trim())) return v;
2274
2314
  return String(getSmartParseNumber(v, axis, layout));
2275
2315
  };
2316
+ /** A path coordinate: required, so an absent one writes `0` as it always has; a guide name passes through. */
2317
+ const pathCoord = (v, axis) => adjCoord(v, axis) ?? "0";
2318
+ /** An `ST_AdjAngle` in degrees, or a guide name (any string) emitted verbatim. */
2319
+ const pathAngle = (v, name) => typeof v === "string" ? v : convertArcAngle(v, name);
2276
2320
  /** `<a:pt>` in the object's own EMU space. */
2277
2321
  const pt = (x, y) => voidEl("a:pt", {
2278
- x: getSmartParseNumber(x, "X", layout),
2279
- y: getSmartParseNumber(y, "Y", layout)
2322
+ x: pathCoord(x, "X"),
2323
+ y: pathCoord(y, "Y")
2280
2324
  }, SPACE_BEFORE_SLASH);
2281
2325
  const nodes = [];
2282
2326
  points?.forEach((point, i) => {
2283
2327
  if (isArcPoint(point)) {
2284
- if ("x" in point || "y" in point) warn("geometry/arc-node-point-ignored", "freeform arc node: x/y are ignored — an arcTo end point is computed from stAng/swAng and the radii.");
2328
+ if ("x" in point || "y" in point) warn("geometry/arc-node-point-ignored", "freeform arc node: x/y are ignored because an arcTo end point is computed from stAng/swAng and the radii.");
2285
2329
  nodes.push(voidEl("a:arcTo", {
2286
- hR: getSmartParseNumber(point.curve.hR, "Y", layout),
2287
- wR: getSmartParseNumber(point.curve.wR, "X", layout),
2288
- stAng: convertArcAngle(point.curve.stAng, "stAng"),
2289
- swAng: convertArcAngle(point.curve.swAng, "swAng")
2330
+ hR: pathCoord(point.curve.hR, "Y"),
2331
+ wR: pathCoord(point.curve.wR, "X"),
2332
+ stAng: pathAngle(point.curve.stAng, "stAng"),
2333
+ swAng: pathAngle(point.curve.swAng, "swAng")
2290
2334
  }, SPACE_BEFORE_SLASH));
2291
2335
  } else if ("curve" in point) switch (point.curve.type) {
2292
2336
  case "cubic":
@@ -2380,7 +2424,7 @@ function genXmlCustGeom(options, cx, cy, layout) {
2380
2424
  ].map(raw));
2381
2425
  }
2382
2426
  /**
2383
- * `<a:prstGeom prst="rect"><a:avLst/></a:prstGeom>` — the unadjusted rectangle every emitter
2427
+ * `<a:prstGeom prst="rect"><a:avLst/></a:prstGeom>`: the unadjusted rectangle every emitter
2384
2428
  * that draws a plain box writes, seven of them by hand.
2385
2429
  *
2386
2430
  * `fmt` carries the caller's `openPrefix`, because indentation before this element is
@@ -2389,8 +2433,16 @@ function genXmlCustGeom(options, cx, cy, layout) {
2389
2433
  function prstGeomRect(fmt) {
2390
2434
  return el("a:prstGeom", { prst: "rect" }, raw(voidEl("a:avLst")), fmt);
2391
2435
  }
2392
- //#endregion
2393
- //#region src/gen/slide/objects/shared.ts
2436
+ // src/gen/slide/objects/shared.ts
2437
+ /**
2438
+ * The colour text the definer marked `_slideColor` is written in on this page: the slide's
2439
+ * {@link Slide.color}, else black. Read here, at write time, so an assignment made after the text
2440
+ * was added still reaches it (#88). A layout or the master has no `color` and gives black.
2441
+ * @param page - the page being written
2442
+ */
2443
+ function slideTextColor(page) {
2444
+ return "color" in page && page.color || "000000";
2445
+ }
2394
2446
  /**
2395
2447
  * The `<p:cNvPr>` every shape renderer writes: self-closing for most shape kinds, paired around a
2396
2448
  * hyperlink or media-action child for the others.
@@ -2491,7 +2543,7 @@ function xfrmEl(tag, frame, attrs = null, fmt) {
2491
2543
  * A group's `<a:xfrm>`: the box, then an IDENTITY child coordinate space (`chOff`/`chExt` equal
2492
2544
  * to `off`/`ext`).
2493
2545
  *
2494
- * The identity is the contract `docs/groups.md` and the measured-fit solver both rest on — a
2546
+ * The identity is the contract `docs/groups.md` and the measured-fit solver both rest on: a
2495
2547
  * group never scales its children, so a grouped shape's authored size is its rendered size.
2496
2548
  * Three group emitters spelled the four-child block out.
2497
2549
  * @param frame - the box, in EMU
@@ -2551,13 +2603,13 @@ function spTreeEl(children) {
2551
2603
  *
2552
2604
  * The child order is the point. `CT_GraphicalObjectFrame` sequences
2553
2605
  * `nvGraphicFramePr` → `xfrm` → `graphic`, and PowerPoint reports a frame that gets it wrong as
2554
- * a corrupt file (0x80070570) rather than as a bad element — a failure the five emitters that
2606
+ * a corrupt file (0x80070570) rather than as a bad element, a failure the five emitters that
2555
2607
  * build one (chart, table, OLE, zoom, 3D model) each had to be right about separately.
2556
2608
  *
2557
2609
  * `nvGraphicFramePr` stays the caller's to build and is *not* folded in here. The five differ in
2558
- * every part of it — an empty `<p:cNvGraphicFramePr>` for a chart against four different
2610
+ * every part of it (an empty `<p:cNvGraphicFramePr>` for a chart against four different
2559
2611
  * `<a:graphicFrameLocks>` default sets, a `<p:nvPr>` that is empty for three and carries a
2560
- * placeholder (and, for a table, a `p14:modId` extension) for the other two — and each of those
2612
+ * placeholder (and, for a table, a `p14:modId` extension) for the other two), and each of those
2561
2613
  * differences is deliberate and documented where it is made.
2562
2614
  *
2563
2615
  * @param opts.nvGraphicFramePr - the already-serialized `<p:nvGraphicFramePr>`
@@ -2580,7 +2632,7 @@ function graphicFrameEl(opts) {
2580
2632
  *
2581
2633
  * Fixed rather than taken from the caller's `objectLock`, and that is true of every construct
2582
2634
  * that emits one: the fallback picture is what a consumer *without* the feature draws, while the
2583
- * caller's locks belong on the `mc:Choice` frame — the object PowerPoint itself manipulates. The
2635
+ * caller's locks belong on the `mc:Choice` frame, the object PowerPoint itself manipulates. The
2584
2636
  * two element types also accept different flags, so folding a `graphicFrameLocks` set onto a
2585
2637
  * `picLocks` would warn about every flag they do not share. Routed through `genXmlObjectLock` so
2586
2638
  * attribute order comes from `PICTURE_LOCK_ATTRS` (`gen/drawingml/locks.ts`) rather than from a literal, and a flag
@@ -2608,22 +2660,21 @@ function previewPicBody(previewRid, frame, outline = false, prefix = { ns: "p" }
2608
2660
  outline ? raw(el("a:ln", { w: "3175" }, raw(el("a:solidFill", null, raw(voidEl("a:prstClr", { val: "ltGray" })))))) : null
2609
2661
  ]);
2610
2662
  }
2611
- //#endregion
2612
- //#region src/gen/opc/part-paths.ts
2663
+ // src/gen/opc/part-paths.ts
2613
2664
  /**
2614
2665
  * Where the parts of a written package live.
2615
2666
  *
2616
2667
  * Every part is named at least three times: the assembler writes it, `[Content_Types].xml`
2617
2668
  * declares an `Override` for it, and one or more `.rels` parts target it. Each of those was
2618
- * its own template literal in its own module, and the three spellings differ — a zip entry
2669
+ * its own template literal in its own module, and the three spellings differ (a zip entry
2619
2670
  * name, a leading-slash part name, a relationship target resolved against the part that
2620
- * states it — so nothing tied them together. A part written but not declared is a
2671
+ * states it), so nothing tied them together. A part written but not declared is a
2621
2672
  * PowerPoint repair, and so is a relationship pointing at a part nobody wrote.
2622
2673
  *
2623
2674
  * The canonical spelling here is the zip entry name: `ppt/slides/slide1.xml`, no leading
2624
- * slash, which is what {@link ZipWriter.add} takes. The other two are derivations —
2675
+ * slash, which is what {@link ZipWriter.add} takes. The other two are derivations:
2625
2676
  * {@link overrideName} for the content-types declaration, {@link targetFromPresentation} and
2626
- * {@link targetFromPptSubpart} for a relationship target — so a path can only be spelled once
2677
+ * {@link targetFromPptSubpart} for a relationship target. So a path can only be spelled once
2627
2678
  * and then transformed, never re-typed in a second dialect.
2628
2679
  *
2629
2680
  * Indices are 1-based throughout, matching the OPC naming rather than the arrays that drive it.
@@ -2644,7 +2695,7 @@ function slideLayoutPath(n) {
2644
2695
  }
2645
2696
  /**
2646
2697
  * The `n`-th notes-slide part. One is written for every slide, empty notes included, so the
2647
- * index is the slide's own — see the assembler, which keeps the two enumerations in step.
2698
+ * index is the slide's own; see the assembler, which keeps the two enumerations in step.
2648
2699
  */
2649
2700
  function notesSlidePath(n) {
2650
2701
  return `ppt/notesSlides/notesSlide${n}.xml`;
@@ -2676,14 +2727,14 @@ function overrideName(path) {
2676
2727
  return `/${path}`;
2677
2728
  }
2678
2729
  /**
2679
- * `path` as a relationship target stated by a part directly under `ppt/` — presentation.xml,
2730
+ * `path` as a relationship target stated by a part directly under `ppt/`: presentation.xml,
2680
2731
  * whose rels reach `slides/slide1.xml` with no traversal at all.
2681
2732
  */
2682
2733
  function targetFromPresentation(path) {
2683
2734
  return path.slice(4);
2684
2735
  }
2685
2736
  /**
2686
- * `path` as a relationship target stated by a part one level below `ppt/` — a slide, a layout,
2737
+ * `path` as a relationship target stated by a part one level below `ppt/`: a slide, a layout,
2687
2738
  * a notes slide, the master: `../slides/slide1.xml`.
2688
2739
  */
2689
2740
  function targetFromPptSubpart(path) {
@@ -2699,7 +2750,7 @@ function targetFromPptSubpart(path) {
2699
2750
  *
2700
2751
  * It exists because the slide-link target used to be spelled `slide${n}.xml` unconditionally, by
2701
2752
  * a function that also serves layouts and the master. A relationship target resolves against the
2702
- * part that states it, so from `ppt/slideLayouts/` that named `ppt/slideLayouts/slide1.xml` — a
2753
+ * part that states it, so from `ppt/slideLayouts/` that named `ppt/slideLayouts/slide1.xml`, a
2703
2754
  * part no package has. Deriving the target from the emitting part's own path means the question
2704
2755
  * is asked once, by the only code that can answer it.
2705
2756
  */
@@ -2707,14 +2758,13 @@ function targetBetweenPptSubparts(fromPath, path) {
2707
2758
  const dir = fromPath.slice(0, fromPath.lastIndexOf("/") + 1);
2708
2759
  return path.startsWith(dir) ? path.slice(dir.length) : targetFromPptSubpart(path);
2709
2760
  }
2710
- //#endregion
2711
- //#region src/gen/opc/rels.ts
2761
+ // src/gen/opc/rels.ts
2712
2762
  /**
2713
2763
  * pptx-ts: shared `Relationship` writers.
2714
2764
  *
2715
2765
  * A `.rels` part is mostly one-off entries, written where the part that needs
2716
2766
  * them is built. This module holds the ones **more than one part** writes, so
2717
- * that the attribute set stays a single fact — a `Relationship` whose `Type` and
2767
+ * that the attribute set stays a single fact. A `Relationship` whose `Type` and
2718
2768
  * `TargetMode` disagree between two writers does not throw, it produces a link
2719
2769
  * PowerPoint silently drops.
2720
2770
  */
@@ -2726,7 +2776,7 @@ function targetBetweenPptSubparts(fromPath, path) {
2726
2776
  * passed `rId3` or `3`, with seven more written inline in `gen/slide/`. The attribute set and
2727
2777
  * its order are byte-significant, so one writer is the only way they stay one fact.
2728
2778
  *
2729
- * `target` is passed **unescaped** — `voidEl` escapes it on the way out. `gen/slide/object.ts`
2779
+ * `target` is passed **unescaped**; `voidEl` escapes it on the way out. `gen/slide/object.ts`
2730
2780
  * depends on that: it computes the escaped form separately to compare a rel against the bytes
2731
2781
  * already emitted, and the two escapers must not drift.
2732
2782
  * @param {string | number} id - the relationship id; a number gets the `rId` prefix
@@ -2756,7 +2806,7 @@ function relationshipsEl(rels, fmt) {
2756
2806
  /**
2757
2807
  * A complete `.rels` part: the XML declaration, a CRLF, then {@link relationshipsEl}.
2758
2808
  *
2759
- * The embedded workbook's parts are the exception and use {@link relationshipsEl} directly —
2809
+ * The embedded workbook's parts are the exception and use {@link relationshipsEl} directly:
2760
2810
  * they follow the declaration with nothing, and two of them end with a bare newline.
2761
2811
  */
2762
2812
  function relationshipsPart(rels, fmt) {
@@ -2774,14 +2824,13 @@ function relationshipsPart(rels, fmt) {
2774
2824
  function externalHyperlinkRel(rId, target) {
2775
2825
  return relationshipEl(rId, HYPERLINK_REL, target, { targetMode: "External" });
2776
2826
  }
2777
- //#endregion
2778
- //#region src/gen/drawingml/list-style.ts
2827
+ // src/gen/drawingml/list-style.ts
2779
2828
  /**
2780
2829
  * pptx-ts: the `<a:lvlNpPr>` list-style level and the theme-font `<a:defRPr>` inside it.
2781
2830
  *
2782
- * A deck states its text defaults in four places — the presentation's `p:defaultTextStyle`, the
2783
- * notesMaster's `p:notesStyle`, and the slide master's built-in and configured `p:txStyles` —
2784
- * and all four write the same level element around the same run-property block: a `solidFill`
2831
+ * A deck states its text defaults in four places: the presentation's `p:defaultTextStyle`, the
2832
+ * notesMaster's `p:notesStyle`, and the slide master's built-in and configured `p:txStyles`.
2833
+ * All four write the same level element around the same run-property block: a `solidFill`
2785
2834
  * over one colour, then the `latin`/`ea`/`cs` triple pointing at one theme font family. They
2786
2835
  * differed in the size, in whether `b`/`i` were carried, and in whether the colour and the
2787
2836
  * Latin face were overridable, which is what the parameters here are.
@@ -2818,8 +2867,7 @@ function lvlPPr(level, attrs, children) {
2818
2867
  ...LEVEL_PPR_TAIL
2819
2868
  }, children);
2820
2869
  }
2821
- //#endregion
2822
- //#region src/gen/slide/notes.ts
2870
+ // src/gen/slide/notes.ts
2823
2871
  /**
2824
2872
  * Collect the speaker-notes runs for a slide (flattened across any number of `addNotes()` calls).
2825
2873
  * @param {PresSlideInternal} slide - the slide object
@@ -2844,8 +2892,8 @@ function getNotesRuns(slide) {
2844
2892
  * can be written twice or gain notes between writes. Each notes-part writer computes this once and
2845
2893
  * hands the result to both the body and the rels file, which is what keeps the two in agreement.
2846
2894
  *
2847
- * Only external `url` hyperlinks are supported; any other target is left off the copy, and a
2848
- * `slide` target says so.
2895
+ * A `url` takes a hyperlink relationship and a `slide` a slide relationship, as on a slide; a show
2896
+ * jump (`action`) takes none.
2849
2897
  * @param {PresSlideInternal} slide - the slide object
2850
2898
  * @return {NotesSlideContent} the notes hyperlink relationships and the runs referencing them
2851
2899
  */
@@ -2857,15 +2905,7 @@ function buildNotesSlideRels(slide) {
2857
2905
  rels,
2858
2906
  runs: getNotesRuns(slide).map((run) => {
2859
2907
  const hyperlink = run.options?.hyperlink;
2860
- if (!hyperlink || !run.options) return run;
2861
- if (!hyperlink.url) {
2862
- if (hyperlink.slide) warn("notes/hyperlink-slide-unsupported", "notes hyperlinks support `url` only (ignoring `slide` target)");
2863
- const { hyperlink: _unsupported, ...options } = run.options;
2864
- return {
2865
- ...run,
2866
- options
2867
- };
2868
- }
2908
+ if (!hyperlink || !run.options || !(hyperlink.url || hyperlink.slide)) return run;
2869
2909
  lastRid++;
2870
2910
  rels.push(hyperlinkRel(lastRid, hyperlink));
2871
2911
  const linked = {
@@ -2909,8 +2949,8 @@ function genXmlNotesParagraphs(runs) {
2909
2949
  /**
2910
2950
  * PowerPoint's creation-id extension, on the `p:cSld` of both notes parts.
2911
2951
  *
2912
- * The value is a fixed literal, and that is the point of naming it: it looks like data — a
2913
- * per-deck identifier PowerPoint would vary — so left inline in two places, someone eventually
2952
+ * The value is a fixed literal, and that is the point of naming it: it looks like data (a
2953
+ * per-deck identifier PowerPoint would vary), so left inline in two places, someone eventually
2914
2954
  * changes one of them. It is deliberately constant, which is what keeps the two notes parts
2915
2955
  * byte-identical across builds.
2916
2956
  */
@@ -3158,8 +3198,8 @@ function makeXmlNotesSlide(slide, notes) {
3158
3198
  /**
3159
3199
  * The notes slide's fixed frame, with the only two slide-dependent strings passed in.
3160
3200
  *
3161
- * A notes slide is the same three placeholders every time — the thumbnail (`sldImg`),
3162
- * the notes body, and the slide-number field — so everything except the body's
3201
+ * A notes slide is the same three placeholders every time: the thumbnail (`sldImg`),
3202
+ * the notes body, and the slide-number field. So everything except the body's
3163
3203
  * paragraphs and the cached slide number is a constant. It is a parameter of its own
3164
3204
  * because the read model authors notes onto a *loaded* deck (`Slide.addNotes`), where
3165
3205
  * there is no `PresSlideInternal` to serialize but the part must come out identical:
@@ -3238,7 +3278,7 @@ function makeXmlNotesSlideSkeleton(bodyParagraphsXml, slideNum) {
3238
3278
  * @return {string} XML
3239
3279
  */
3240
3280
  function makeXmlNotesSlideRel(notes, slideNumber) {
3241
- const hlinkRels = notes.rels.map((rel) => externalHyperlinkRel(rel.rId, rel.Target)).join("");
3281
+ const hlinkRels = notes.rels.map((rel) => rel.data === "slide" ? relationshipEl(rel.rId, SLIDE_REL, targetFromPptSubpart(slidePath(Number(rel.Target)))) : externalHyperlinkRel(rel.rId, rel.Target)).join("");
3242
3282
  return XML_DECL + relationshipsEl([
3243
3283
  relationshipEl(1, NOTES_MASTER_REL, targetFromPptSubpart(NOTES_MASTER_PATH)),
3244
3284
  relationshipEl(2, SLIDE_REL, targetFromPptSubpart(slidePath(slideNumber))),
@@ -3259,7 +3299,95 @@ function makeXmlNotesMasterRel() {
3259
3299
  closePrefix: "\n "
3260
3300
  });
3261
3301
  }
3262
- //#endregion
3263
- export { noStrokeLine as $, graphicFrameEl as A, getDuplicateObjectNames as At, genXmlParagraphProperties as B, targetBetweenPptSubparts as C, FONT_DATA_CONTENT_TYPE as Ct, cNvPrEl as D, serializeEmbeddedFontLst as Dt, FALLBACK_PICTURE_LOCKS as E, flattenEmbeddedFaces as Et, assertKnownPreset as F, nextMediaTarget as Ft, clampParaMarginInchesEmu as G, renderTextParagraphsXml as H, genXmlCustGeom as I, preencodedPath as It, createHyperlinkRels as J, buAutoNumEl as K, genXmlPresetGeom as L, validateObjectName as Lt, previewPicBody as M, getUuid as Mt, spTreeEl as N, isHyperlinkRel as Nt, cNvPrHyperlink as O, XML_DECL as Ot, xfrmEl as P, isPreencodedPath as Pt, createLineCap as Q, prstGeomRect as R, encodeXmlAttrValue as Rt, slidePath as S, voidEl as St, targetFromPresentation as T, FONT_REL_TYPE as Tt, clampFontSizeSz as U, groupRunsIntoLines as V, clampParaIndentInchesEmu as W, validateHyperlink as X, registerHyperlinkRel as Y, borderLine as Z, fontPath as _, namedColorOr as _t, makeXmlNotesSlideRel as a, fillNamesPaint as at, relsPath as b, elOrVoid as bt, themeFontDefRPr as c, rejectEmptyFill as ct, relationshipsEl as d, genXmlImageCropRect as dt, resolveBorderDash as et, relationshipsPart as f, createShadowEffectLst as ft, commentPath as g, isColorValue as gt, SLIDE_MASTER_PATH as h, createColorElement as ht, makeXmlNotesSlide as i, withLineDefaults as it, grpXfrmEl as j, getNewRelId as jt, genXmlShapeLine as k, bracedGuid as kt, externalHyperlinkRel as l, resolveFillKind as lt, PRESENTATION_PATH as m, alphaEl as mt, makeXmlNotesMaster as n, strokeDash as nt, makeXmlNotesSlideSkeleton as o, genXmlColorSelection as ot, NOTES_MASTER_PATH as p, normalizeShadowOptions as pt, buCharEl as q, makeXmlNotesMasterRel as r, strokePaint as rt, lvlPPr as s, genXmlPatternFill as st, buildNotesSlideRels as t, resolveDash as tt, relationshipEl as u, STRETCH_FILL_RECT as ut, notesSlidePath as v, rejectEmptyColor as vt, targetFromPptSubpart as w, FONT_DATA_EXTENSION as wt, slideLayoutPath as x, raw as xt, overrideName as y, el as yt, genXmlNormAutofit as z };
3302
+ // src/gen/slide/comments.ts
3303
+ /**
3304
+ * pptx-ts: comment parts
3305
+ *
3306
+ * Resolve every slide's comments into a deck-wide author registry with per-author
3307
+ * numbering, then emit the presentation-level `commentAuthors.xml` and each
3308
+ * slide's `comments/commentN.xml` part.
3309
+ */
3310
+ /**
3311
+ * Walk every slide's comments and build the deck-wide author registry + per-comment numbering.
3312
+ *
3313
+ * Legacy comments number each comment with a *per-author* 1-based `idx`; an author's `lastIdx` is the
3314
+ * highest `idx` it used. Authors are keyed by `name`+`initials` and assigned ids in first-appearance
3315
+ * order (slide order, then insertion order). `clrIdx` mirrors `id` (one colour slot per author).
3316
+ * @param {PresSlideInternal[]} slides - all presentation slides
3317
+ * @return {ResolvedComments} the author list and per-comment metadata
3318
+ */
3319
+ function resolveCommentAuthors(slides) {
3320
+ const byKey = /* @__PURE__ */ new Map();
3321
+ const authors = [];
3322
+ const perAuthorCount = /* @__PURE__ */ new Map();
3323
+ const meta = /* @__PURE__ */ new Map();
3324
+ slides.forEach((slide) => {
3325
+ slide._comments.forEach((comment) => {
3326
+ const key = comment.author + "\0" + comment.initials;
3327
+ let author = byKey.get(key);
3328
+ if (!author) {
3329
+ author = {
3330
+ id: authors.length,
3331
+ name: comment.author,
3332
+ initials: comment.initials,
3333
+ lastIdx: 0,
3334
+ clrIdx: authors.length
3335
+ };
3336
+ byKey.set(key, author);
3337
+ authors.push(author);
3338
+ perAuthorCount.set(author.id, 0);
3339
+ }
3340
+ const idx = (perAuthorCount.get(author.id) ?? 0) + 1;
3341
+ perAuthorCount.set(author.id, idx);
3342
+ author.lastIdx = idx;
3343
+ meta.set(comment, {
3344
+ authorId: author.id,
3345
+ idx
3346
+ });
3347
+ });
3348
+ });
3349
+ return {
3350
+ authors,
3351
+ meta
3352
+ };
3353
+ }
3354
+ /**
3355
+ * Creates the presentation-level `ppt/commentAuthors.xml` part (`<p:cmAuthorLst>`).
3356
+ * @param {ResolvedCommentAuthor[]} authors - resolved deck-wide author registry
3357
+ * @return {string} XML
3358
+ */
3359
+ function makeXmlCommentAuthors(authors) {
3360
+ const cmAuthors = authors.map((author) => voidEl("p:cmAuthor", {
3361
+ id: author.id,
3362
+ name: author.name,
3363
+ initials: author.initials,
3364
+ lastIdx: author.lastIdx,
3365
+ clrIdx: author.clrIdx
3366
+ })).join("");
3367
+ return XML_DECL + "\r\n" + el("p:cmAuthorLst", { ...PML_ROOT_NS }, raw(cmAuthors));
3368
+ }
3369
+ /**
3370
+ * Creates a per-slide comments part `ppt/comments/comment{N}.xml` (`<p:cmLst>`).
3371
+ * @param {SlideComment[]} comments - the slide's comments, in order
3372
+ * @param {Map} meta - per-comment `authorId`/`idx`, from {@link resolveCommentAuthors} on a write
3373
+ * and from the destination's registry on an append
3374
+ * @return {string} XML
3375
+ */
3376
+ function makeXmlComments(comments, meta) {
3377
+ const cms = comments.map((comment) => {
3378
+ const m = meta.get(comment);
3379
+ if (!m) throw new InternalError("comment/not-resolved", "makeXmlComments: a comment has no author metadata");
3380
+ return el("p:cm", {
3381
+ authorId: m.authorId,
3382
+ dt: comment.date || null,
3383
+ idx: m.idx
3384
+ }, [raw(voidEl("p:pos", {
3385
+ x: Math.round(inch2Emu(comment.x)),
3386
+ y: Math.round(inch2Emu(comment.y))
3387
+ })), raw(el("p:text", null, comment.text))]);
3388
+ }).join("");
3389
+ return XML_DECL + "\r\n" + el("p:cmLst", { ...PML_ROOT_NS }, raw(cms));
3390
+ }
3391
+ export { buAutoNumEl as $, cNvPrEl as A, namedColorOr as At, genXmlCustGeom as B, getUuid as Bt, relsPath as C, resolveFillKind as Ct, targetFromPptSubpart as D, alphaEl as Dt, targetBetweenPptSubparts as E, genXmlImageCropRect as Et, previewPicBody as F, voidEl as Ft, groupRunsIntoLines as G, validateObjectName as Gt, prstGeomRect as H, isPreencodedPath as Ht, slideTextColor as I, XML_DECL as It, clampFontSizeSz as J, renderTextParagraphsXml as K, encodeXmlAttrValue as Kt, spTreeEl as L, bracedGuid as Lt, genXmlShapeLine as M, el as Mt, graphicFrameEl as N, elOrVoid as Nt, targetFromPresentation as O, createColorElement as Ot, grpXfrmEl as P, raw as Pt, clampParaMarginInchesEmu as Q, xfrmEl as R, getDuplicateObjectNames as Rt, overrideName as S, rejectEmptyFill as St, slidePath as T, STRETCH_FILL_RECT as Tt, genXmlNormAutofit as U, nextMediaTarget as Ut, genXmlPresetGeom as V, isHyperlinkRel as Vt, genXmlParagraphProperties as W, preencodedPath as Wt, clampParaIndentInchesEmu as X, clampParaIndentEmu as Y, clampParaMarginEmu as Z, PRESENTATION_PATH as _, flattenEmbeddedFaces as _t, makeXmlNotesMaster as a, createLineCap as at, fontPath as b, genXmlColorSelection as bt, makeXmlNotesSlideRel as c, resolveDash as ct, themeFontDefRPr as d, withLineDefaults as dt, buCharEl as et, externalHyperlinkRel as f, createShadowEffectLst as ft, NOTES_MASTER_PATH as g, FONT_REL_TYPE as gt, relationshipsPart as h, FONT_DATA_EXTENSION as ht, buildNotesSlideRels as i, borderLine as it, cNvPrHyperlink as j, rejectEmptyColor as jt, FALLBACK_PICTURE_LOCKS as k, isColorValue as kt, makeXmlNotesSlideSkeleton as l, strokeDash as lt, relationshipsEl as m, FONT_DATA_CONTENT_TYPE as mt, makeXmlComments as n, registerHyperlinkRel as nt, makeXmlNotesMasterRel as o, noStrokeLine as ot, relationshipEl as p, normalizeShadowOptions as pt, underlineProps as q, resolveCommentAuthors as r, validateHyperlink as rt, makeXmlNotesSlide as s, resolveBorderDash as st, makeXmlCommentAuthors as t, createHyperlinkRels as tt, lvlPPr as u, strokePaint as ut, SLIDE_MASTER_PATH as v, serializeEmbeddedFontLst as vt, slideLayoutPath as w, solidPaint as wt, notesSlidePath as x, genXmlPatternFill as xt, commentPath as y, fillNamesPaint as yt, assertKnownPreset as z, getNewRelId as zt };
3264
3392
 
3265
- //# sourceMappingURL=notes-BcT571R4.js.map
3393
+ //# sourceMappingURL=comments-CKIZYRbZ.js.map