@shbernal/ts-pptx 4.0.1 → 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 +147 -114
  2. package/dist/anim-presets-Qbgh8uut.js +55 -0
  3. package/dist/anim-presets-Qbgh8uut.js.map +1 -0
  4. package/dist/{autopage-odPz7zZO.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-Dw_BeWD_.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-CvN2iCA4.js → chart-metadata-CwiTTcJq.js} +634 -537
  15. package/dist/chart-metadata-CwiTTcJq.js.map +1 -0
  16. package/dist/{clip-Bl01BPcN.d.ts → clip-ISfCMtWA.d.ts} +6 -10
  17. package/dist/clip-ISfCMtWA.d.ts.map +1 -0
  18. package/dist/{notes-Dh12JAbn.js → comments-CKIZYRbZ.js} +530 -401
  19. package/dist/comments-CKIZYRbZ.js.map +1 -0
  20. package/dist/{diagnostics-D8ot4Ei7.d.ts → diagnostics-MoA4MBq5.d.ts} +55 -53
  21. package/dist/diagnostics-MoA4MBq5.d.ts.map +1 -0
  22. package/dist/{embedded-fonts-DarT9aoa.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-DCnvBZz2.d.ts → entry-compose-CiakSt-f.d.ts} +62 -81
  25. package/dist/entry-compose-CiakSt-f.d.ts.map +1 -0
  26. package/dist/{errors-C4oENPcu.js → entry-errors-JMthgJ2g.js} +45 -29
  27. package/dist/entry-errors-JMthgJ2g.js.map +1 -0
  28. package/dist/{table-cell-margins-EVg0EwxT.js → enums-BP9wsb9Q.js} +44 -47
  29. package/dist/enums-BP9wsb9Q.js.map +1 -0
  30. package/dist/{enums-BUKTUB6V.d.ts → enums-PcXr3o3J.d.ts} +13 -14
  31. package/dist/{enums-BUKTUB6V.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-Cb7naFj6.js → fit-Bpmzep6O.js} +64 -56
  36. package/dist/fit-Bpmzep6O.js.map +1 -0
  37. package/dist/{font-metrics-v5fn7s7e.d.ts → font-metrics-DTppw4t3.d.ts} +62 -38
  38. package/dist/font-metrics-DTppw4t3.d.ts.map +1 -0
  39. package/dist/{grid-DikTjFey.js → grid-DUIMxjfl.js} +23 -26
  40. package/dist/grid-DUIMxjfl.js.map +1 -0
  41. package/dist/{html-dom-Iuog2wdK.js → html-dom-LZCgntnJ.js} +46 -47
  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 +16 -18
  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-CrYXAfUu.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-J49Ck6S2.d.ts → package-CANBWuSm.d.ts} +9 -13
  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-C6qgOFAa.d.ts → pres-BkEkUAEM.d.ts} +410 -237
  75. package/dist/pres-BkEkUAEM.d.ts.map +1 -0
  76. package/dist/{presentation-DGYlAWhl.js → presentation-4Ggq0_Xa.js} +470 -315
  77. package/dist/presentation-4Ggq0_Xa.js.map +1 -0
  78. package/dist/{presentation-types-8jLQHI4M.d.ts → presentation-types-BgnZR-0a.d.ts} +1119 -468
  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 +2700 -780
  86. package/dist/script.js.map +1 -1
  87. package/dist/{shapes-re0KyQk_.js → shapes-CAPOjU0Q.js} +1564 -491
  88. package/dist/shapes-CAPOjU0Q.js.map +1 -0
  89. package/dist/{slide-Dx2ZeVZ6.d.ts → slide-BmT4kMoO.d.ts} +102 -89
  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-BTDP3lEP.js → table-dom-BYga4FYY.js} +4 -5
  94. package/dist/{table-dom-BTDP3lEP.js.map → table-dom-BYga4FYY.js.map} +1 -1
  95. package/dist/{text-sN64Otf_.js → text-CU08xJGs.js} +393 -344
  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-XJlf-hOl.js → units-internal-BR3a4cIi.js} +34 -32
  100. package/dist/units-internal-BR3a4cIi.js.map +1 -0
  101. package/dist/{zip-CKd8ihcf.js → zip-7JTh61LX.js} +8 -10
  102. package/dist/zip-7JTh61LX.js.map +1 -0
  103. package/dist/{zip-C0leZZMj.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-DPUHh85d.js → zoom-CSSeOwM8.js} +1204 -620
  108. package/dist/zoom-CSSeOwM8.js.map +1 -0
  109. package/package.json +27 -28
  110. package/skills/{ts-pptx-upstream → pptx-ts-upstream}/SKILL.md +37 -84
  111. package/dist/autopage-odPz7zZO.js.map +0 -1
  112. package/dist/binary-string-CKVAYI3M.js.map +0 -1
  113. package/dist/body-insets-Dw_BeWD_.js.map +0 -1
  114. package/dist/clip-Bl01BPcN.d.ts.map +0 -1
  115. package/dist/diagnostics-D8ot4Ei7.d.ts.map +0 -1
  116. package/dist/embedded-fonts-DarT9aoa.d.ts.map +0 -1
  117. package/dist/entry-compose-DCnvBZz2.d.ts.map +0 -1
  118. package/dist/errors-C4oENPcu.js.map +0 -1
  119. package/dist/fit-Cb7naFj6.js.map +0 -1
  120. package/dist/font-metrics-v5fn7s7e.d.ts.map +0 -1
  121. package/dist/grid-DikTjFey.js.map +0 -1
  122. package/dist/html-dom-Iuog2wdK.js.map +0 -1
  123. package/dist/image-size-CvN2iCA4.js.map +0 -1
  124. package/dist/notes-Dh12JAbn.js.map +0 -1
  125. package/dist/options-internal-WqgMf720.js.map +0 -1
  126. package/dist/package-J49Ck6S2.d.ts.map +0 -1
  127. package/dist/playbtn-CUqQdoDf.js.map +0 -1
  128. package/dist/pres-C6qgOFAa.d.ts.map +0 -1
  129. package/dist/presentation-DGYlAWhl.js.map +0 -1
  130. package/dist/presentation-types-8jLQHI4M.d.ts.map +0 -1
  131. package/dist/shapes-re0KyQk_.js.map +0 -1
  132. package/dist/slide-Dx2ZeVZ6.d.ts.map +0 -1
  133. package/dist/table-cell-margins-EVg0EwxT.js.map +0 -1
  134. package/dist/text-anchor-CrYXAfUu.js.map +0 -1
  135. package/dist/text-sN64Otf_.js.map +0 -1
  136. package/dist/units-internal-XJlf-hOl.js.map +0 -1
  137. package/dist/zip-C0leZZMj.d.ts.map +0 -1
  138. package/dist/zip-CKd8ihcf.js.map +0 -1
  139. package/dist/zoom-DPUHh85d.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-C4oENPcu.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-XJlf-hOl.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-EVg0EwxT.js";
4
- import { $t as THEME_REL, Ct as HYPERLINK_REL, E as EMBEDDED_FONT_SLOTS, F as LINE_END_TYPES, I as PRESET_LINE_DASHES, J as TEXT_TAB_ALIGN_TYPES, Rt as PACKAGE_REL_NS, St as FONT_REL, U as TEXT_AUTONUM_SCHEMES, W as TEXT_CAPS_TYPES, Y as TEXT_UNDERLINE_TYPES, Yt as SLIDE_REL, a as textAlignToken, an as PML_ROOT_NS, d as xsdBool, f as xsdBoolIfTrue, h as stripHash, in as OOXML_NS, jt as NOTES_MASTER_REL, l as checkEnumOrWarn, m as splitRgbaHex, p as isHexColor, q as TEXT_STRIKE_TYPES, s as lineCapToken, w as DEFAULT_COLOR_MAP } from "./image-size-CvN2iCA4.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
- * ts-pptx: turning a caller's string into something that can sit inside XML.
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,10 +78,9 @@ 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
- * ts-pptx: shared generator helpers.
83
+ * pptx-ts: shared generator helpers.
85
84
  *
86
85
  * The small cross-cutting pieces the OOXML writers all need and that belong to
87
86
  * no single part:
@@ -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
284
+ // src/gen/oxml/el.ts
288
285
  /**
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
369
- /**
370
- * ts-pptx: XML element builder (write-side substrate)
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,10 +361,9 @@ 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
- * ts-pptx: DrawingML color elements
366
+ * pptx-ts: DrawingML color elements
452
367
  *
453
368
  * Emit the bare `<a:srgbClr>` / `<a:schemeClr>` color element that every other
454
369
  * color context builds on (solid fills, gradient stops, effect alphas, line
@@ -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,128 +480,9 @@ 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
483
+ // src/gen/drawingml/src-rect.ts
570
484
  /**
571
- * ts-pptx: 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
688
- /**
689
- * ts-pptx: the explicit `<a:srcRect>` source crop
485
+ * pptx-ts: the explicit `<a:srcRect>` source crop
690
486
  *
691
487
  * A leaf module on purpose. A picture object (`addImage`) and a native picture fill
692
488
  * (`fill: { type: 'image' }`) crop a source image the same way, but they live on opposite
@@ -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,10 +548,9 @@ 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
- * ts-pptx: DrawingML fills
553
+ * pptx-ts: DrawingML fills
757
554
  *
758
555
  * Emit the fill group a shape, cell, chart element, or line paint can carry:
759
556
  * `<a:solidFill>`, `<a:gradFill>`, `<a:pattFill>`, `<a:blipFill>`.
@@ -814,9 +611,10 @@ function genXmlGradientFill(gradient) {
814
611
  */
815
612
  function genXmlPatternFill(pattern) {
816
613
  if (!pattern) throw new InvalidOptionError("pattern-fill/missing-pattern", "Pattern fill requires a pattern object.");
614
+ const preset = checkEnumOrThrow(pattern.preset, PATTERN_PRESETS, "pattern preset", "pattern-fill/invalid-preset");
817
615
  const fgColor = namedColorOr(pattern.fgColor, "000000", "pattern.fgColor");
818
616
  const bgColor = namedColorOr(pattern.bgColor, "FFFFFF", "pattern.bgColor");
819
- return el("a:pattFill", { prst: pattern.preset }, [raw(el("a:fgClr", null, raw(createColorElement(fgColor)))), raw(el("a:bgClr", null, raw(createColorElement(bgColor))))]);
617
+ return el("a:pattFill", { prst: preset }, [raw(el("a:fgClr", null, raw(createColorElement(fgColor)))), raw(el("a:bgClr", null, raw(createColorElement(bgColor))))]);
820
618
  }
821
619
  /**
822
620
  * Create a native DrawingML picture (image) fill.
@@ -850,7 +648,7 @@ function genXmlImageFill(props) {
850
648
  * `type: 'none'` needs in order to mean anything (`{ type: 'none', color }` must stay
851
649
  * transparent), so it is the rule everywhere rather than one kind's exception.
852
650
  *
853
- * 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:
854
652
  * `gradient`, then `pattern`, then `image`. Setting two without a `type` is already a
855
653
  * contradiction; the order exists so the answer is stated rather than emergent.
856
654
  *
@@ -872,15 +670,15 @@ function resolveFillKind(props) {
872
670
  /**
873
671
  * {@link resolveFillKind} for a stroke, which has one kind fewer.
874
672
  *
875
- * `<a:ln>`'s paint child is `EG_LineFillProperties` — `a:noFill`, `a:solidFill`, `a:gradFill`,
876
- * `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
877
675
  * and `ShapeLineProps` subtracts `image` from the fill props it inherits. This is where the
878
676
  * subtraction is enforced at run time, for the JS caller TypeScript cannot stop: the request
879
677
  * is refused rather than painted as nothing, because emitting a blipFill inside `a:ln` is a
880
678
  * package PowerPoint reports as needing repair, and emitting nothing is a stroke the caller
881
679
  * asked to be a picture and got in the theme colour without being told.
882
680
  *
883
- * 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
884
682
  * the refusal names the call the caller made rather than surfacing at serialization time.
885
683
  */
886
684
  function resolveLineKind(props) {
@@ -896,7 +694,7 @@ function resolveLineKind(props) {
896
694
  * a fill child for an empty props object would paint them default black.
897
695
  *
898
696
  * "Said nothing" is narrower than "is empty". `'inherit'` is the explicit spelling of the
899
- * 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.
900
698
  * A `'solid'` kind needs a `color` to be saying anything; every other kind carries its
901
699
  * payload in its own sub-object, so its presence is the statement.
902
700
  *
@@ -915,7 +713,7 @@ function fillNamesPaint(props) {
915
713
  /**
916
714
  * Reject the empty-colour spelling of a missing value, in either shape a fill can wear.
917
715
  *
918
- * `''` 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
919
717
  * second used to reach {@link createColorElement} and paint black. Both fold back onto
920
718
  * *absence* here, which is not the same as "emit nothing": what an absent fill means is the
921
719
  * surface's own rule, and a text box's absent fill is `<a:noFill/>`. So a site whose absence
@@ -932,8 +730,8 @@ function rejectEmptyFill(props, option) {
932
730
  /**
933
731
  * A solid paint for {@link genXmlColorSelection}, carrying `transparency` only when one was set.
934
732
  *
935
- * Every caller assembles this out of an optional source — `border.transparency`,
936
- * `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
937
735
  * of "no transparency": one that reads the same here, but not in a bag that is spread over
938
736
  * another. So the key is simply absent, which is this codebase's one spelling of unset.
939
737
  * @param color - the colour to paint, already resolved against whatever default applies
@@ -948,7 +746,7 @@ function solidPaint(color, transparency) {
948
746
  /**
949
747
  * Emit the fill group a set of fill/line props asks for, dispatching on
950
748
  * {@link resolveFillKind}. Returns '' for props that name nothing to paint, so a caller
951
- * 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`.
952
750
  * @param {Color | ShapeFillProps | ShapeLineProps | undefined} props fill props
953
751
  * @returns XML string
954
752
  */
@@ -988,8 +786,204 @@ function genXmlColorSelection(props) {
988
786
  }
989
787
  return outText;
990
788
  }
991
- //#endregion
992
- //#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
993
987
  /**
994
988
  * Every key `BorderProps` defines.
995
989
  *
@@ -1089,7 +1083,7 @@ function lineEndEl(tag, value, label) {
1089
1083
  *
1090
1084
  * TypeScript's excess-property check already rejects a stray key on a border written
1091
1085
  * *inline* at the call site. It deliberately does not fire when the border is built as
1092
- * 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
1093
1087
  * legitimately be a supertype. That exemption covers precisely the reuse pattern a
1094
1088
  * shared grid style encourages, so the mistake reaches here typed as valid and the
1095
1089
  * wrong-named value is discarded during generation. A `.pptx` gives no other signal:
@@ -1099,7 +1093,7 @@ function lineEndEl(tag, value, label) {
1099
1093
  function warnUnknownBorderKeys(border) {
1100
1094
  for (const key in border) {
1101
1095
  if (!Object.hasOwn(border, key) || BORDER_KEYS.includes(key)) continue;
1102
- 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.`, {
1103
1097
  received: key,
1104
1098
  valid: BORDER_KEYS
1105
1099
  });
@@ -1113,8 +1107,8 @@ function warnUnknownBorderKeys(border) {
1113
1107
  * than taking the default: `lineWidthToEmu` refuses `NaN` and clamps `Infinity` to the top of
1114
1108
  * `ST_LineWidth`, as it does for every other line width.
1115
1109
  *
1116
- * Every `BorderProps` the library reads — table cell borders, table-style regions, and
1117
- * 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
1118
1112
  * place that can vet a border's shape for all of them ({@link warnUnknownBorderKeys}).
1119
1113
  * @param {BorderProps} border - border properties (may carry `width`)
1120
1114
  * @param {number} defaultPt - width to use when `width` is not a number
@@ -1127,7 +1121,7 @@ function resolveBorderWidth(border, defaultPt) {
1127
1121
  }
1128
1122
  /**
1129
1123
  * A shape outline with its defaults applied: the kind `resolveLineKind` answers, no transparency, a
1130
- * 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.
1131
1125
  *
1132
1126
  * Shapes, line-shaped text boxes and connectors each merged these themselves, and the connector
1133
1127
  * disagreed about zero: `line.width: 0` took the 1pt default on a shape and a text box, and wrote no
@@ -1145,6 +1139,10 @@ function resolveBorderWidth(border, defaultPt) {
1145
1139
  */
1146
1140
  function withLineDefaults(line, colorLabel) {
1147
1141
  const kind = resolveLineKind(line);
1142
+ if (kind === "inherit") return {
1143
+ ...line,
1144
+ type: kind
1145
+ };
1148
1146
  const resolved = {
1149
1147
  ...line,
1150
1148
  type: kind,
@@ -1171,7 +1169,7 @@ function createLineCap(lineCap) {
1171
1169
  *
1172
1170
  * `<a:ln>` takes almost the same fill group as a shape interior, so a stroke can be a gradient
1173
1171
  * or a pattern as well as a solid color. Which one is asked for is {@link resolveLineKind}'s
1174
- * 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
1175
1173
  * sub-object while the interior inferred nothing, so the same `{ gradient }` spelling painted a
1176
1174
  * gradient outline and a black fill. "Almost" is the picture fill: `EG_LineFillProperties` has
1177
1175
  * no `a:blipFill`, and `resolveLineKind` is where that is refused.
@@ -1195,20 +1193,20 @@ function genXmlLineFill(line) {
1195
1193
  * Four builders used to turn a `BorderProps` into a line element, each with its own
1196
1194
  * defaults, and the differences between them were only visible by reading all four: a
1197
1195
  * chart data point's outline, a chart-area or plot-area border, the `<c:spPr>` line the
1198
- * plot and chart space share, and a table cell's six edges. They agree on the shape —
1199
- * `w` and `cap` on the element, a solid paint from `color`/`transparency`, then an
1200
- * 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
1201
1199
  * those slots. Those are this function's parameters, so each caller is now a named
1202
1200
  * argument set rather than a re-derivation.
1203
1201
  *
1204
1202
  * Two things are deliberately *not* parameters. A stroke that paints nothing is spelled
1205
1203
  * differently by each caller (a bare `<a:ln><a:noFill/></a:ln>` for a chart, an
1206
1204
  * attribute-carrying `w="0"` rule for a table cell, and nothing at all for a data point,
1207
- * 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
1208
1206
  * spelling of the first. And the colour default is taken on `||`, not `??`, so an empty
1209
1207
  * `color` reads as *unstated* rather than reaching the colour validator as a bad value:
1210
1208
  * three of the four callers used to hand `''` straight through, which warns
1211
- * `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
1212
1210
  * default they had named a line earlier. The chart-border builder already read it this way,
1213
1211
  * and `dataBorder` is the only one of the other three a caller can actually reach with an
1214
1212
  * empty string; the rest resolve their colour in the definition step first.
@@ -1237,7 +1235,7 @@ function borderLine(name, border, spec) {
1237
1235
  * The `<a:ln>` that paints no stroke at all: an explicit `<a:noFill/>` on a bare element.
1238
1236
  *
1239
1237
  * This is the chart side's spelling of "no line" and is a different statement from a
1240
- * {@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
1241
1239
  * `a:lnRef`, where this overrides it. A table cell spells the same intent with the
1242
1240
  * attributes still on the element and `w="0"`, which is why that one is not this.
1243
1241
  * @returns the `<a:ln><a:noFill/></a:ln>` element XML
@@ -1245,13 +1243,12 @@ function borderLine(name, border, spec) {
1245
1243
  function noStrokeLine() {
1246
1244
  return el("a:ln", null, raw(voidEl("a:noFill")));
1247
1245
  }
1248
- //#endregion
1249
- //#region src/gen/define/hyperlinks.ts
1246
+ // src/gen/define/hyperlinks.ts
1250
1247
  /**
1251
1248
  * The relationship record one hyperlink serializes to.
1252
1249
  *
1253
- * `data` distinguishes the two targets a hyperlink rel can have — `'slide'` for an internal
1254
- * 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
1255
1252
  * every emitter escapes it on the way out (see the note on `SlideRel.Target`).
1256
1253
  *
1257
1254
  * Four sites built this record by hand: the two below, `addImage`'s own hyperlink, and the
@@ -1275,7 +1272,8 @@ function hyperlinkRel(rId, hyperlink) {
1275
1272
  * definer and the run emitter each checked their own subset, and the shape path checked nothing. An
1276
1273
  * image refused an action-only link that a shape accepted, and nothing refused `url` together with
1277
1274
  * `slide`: that wrote two `<a:hlinkClick>` into one `<p:cNvPr>`, which allows one, over a slide-typed
1278
- * 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.
1279
1277
  *
1280
1278
  * A `slide` has to be a 1-based slide number. `-1` and `1.5` were written as the targets
1281
1279
  * `slide-1.xml` and `slide1.5.xml`, parts no deck has. Whether the numbered slide exists is only
@@ -1287,17 +1285,23 @@ function hyperlinkRel(rId, hyperlink) {
1287
1285
  function validateHyperlink(hyperlink, call) {
1288
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'}\``);
1289
1287
  const { url, slide, action } = hyperlink;
1290
- 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 ")}.`);
1291
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)}.`);
1292
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)}.`);
1293
1297
  }
1294
1298
  /**
1295
1299
  * Mint a fresh rel id for `hyperlink`, register it on `target`, and stamp the id back onto
1296
1300
  * the hyperlink so the emitter can write `r:id`.
1297
1301
  *
1298
1302
  * The id comes from {@link getNewRelId}, which skips every id already held on the slide.
1299
- * `addImage` used to increment the image's own id instead, which is how an SVG picture — a
1300
- * 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
1301
1305
  * hyperlink and emitting a duplicate `Relationship Id`.
1302
1306
  * @returns the allocated relationship id
1303
1307
  */
@@ -1309,9 +1313,7 @@ function registerHyperlinkRel(target, hyperlink) {
1309
1313
  }
1310
1314
  /** The objects one level of a text or table-cell tree holds: a table row is itself an array. */
1311
1315
  function hyperlinkTreeLevel(text) {
1312
- if (typeof text === "string" || typeof text === "number") return [];
1313
- if (Array.isArray(text)) return text;
1314
- return typeof text === "object" ? [text] : [];
1316
+ return Array.isArray(text) ? text : [text];
1315
1317
  }
1316
1318
  /**
1317
1319
  * Parses text/text-objects from `addText()` and `addTable()` methods; creates 'hyperlink'-type Slide Rels for each hyperlink found
@@ -1320,7 +1322,7 @@ function hyperlinkTreeLevel(text) {
1320
1322
  * refused link on a second run used to throw after the first run's relationship was already on
1321
1323
  * the slide, left pointing at a shape that was never added.
1322
1324
  * @param {PresSlideInternal} target - slide object that any hyperlinks will be be added to
1323
- * @param {number | string | TextProps | TextProps[] | TableCellInternal[][]} text - text to parse
1325
+ * @param {SlideObject | TextProps | TextProps[] | TableCellInternal[][]} text - text to parse
1324
1326
  * @param call - the method the objects were authored through, for a refused hyperlink's message
1325
1327
  */
1326
1328
  function createHyperlinkRels(target, text, call = "addText") {
@@ -1365,8 +1367,7 @@ function registerHyperlinkTree(target, tree) {
1365
1367
  }
1366
1368
  });
1367
1369
  }
1368
- //#endregion
1369
- //#region src/gen/drawingml/bullet.ts
1370
+ // src/gen/drawingml/bullet.ts
1370
1371
  /** A bullet `characterCode`: four hex digits naming a code point. */
1371
1372
  const CHARACTER_CODE = /^[0-9A-Fa-f]{4}$/;
1372
1373
  /** `ST_TextBulletStartAtNum`'s range. */
@@ -1411,13 +1412,12 @@ function buAutoNumEl(type, startAt, label) {
1411
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")
1412
1413
  });
1413
1414
  }
1414
- //#endregion
1415
- //#region src/gen/drawingml/clamp.ts
1415
+ // src/gen/drawingml/clamp.ts
1416
1416
  /**
1417
- * ts-pptx: DrawingML value clamps
1417
+ * pptx-ts: DrawingML value clamps
1418
1418
  *
1419
1419
  * Clamp font/character/line spacing values into their ST_* schema ranges and convert to the
1420
- * 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
1421
1421
  * percent for the one measure the caller states as a multiple rather than a size.
1422
1422
  * Out-of-range values make PowerPoint report the package as needing repair.
1423
1423
  */
@@ -1461,6 +1461,16 @@ const CHAR_SPACING = {
1461
1461
  min: -4e5,
1462
1462
  max: 4e5
1463
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
+ };
1464
1474
  const PARA_MARGIN = {
1465
1475
  code: "text/paragraph-margin-out-of-range",
1466
1476
  nonFiniteCode: "coord/non-finite",
@@ -1534,9 +1544,13 @@ function clampFontSizeSz(fontSizePts, label) {
1534
1544
  function clampCharSpacingSpc(charSpacingPts) {
1535
1545
  return clampWithWarn(charSpacingPts, CHAR_SPACING);
1536
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
+ }
1537
1551
  /**
1538
1552
  * Clamp a paragraph's left margin (points) into ST_TextMargin (0..4032pt, i.e. 0..51206400 EMU);
1539
- * 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;
1540
1554
  * it is a value PowerPoint reports as needing repair.
1541
1555
  */
1542
1556
  function clampParaMarginEmu(marginPts) {
@@ -1586,13 +1600,12 @@ function clampLineSpacingPts(lineSpacingPts) {
1586
1600
  function clampLineSpacingMultiplePct(multiple) {
1587
1601
  return clampWithWarn(multiple, LINE_SPACING_MULTIPLE);
1588
1602
  }
1589
- //#endregion
1590
- //#region src/gen/drawingml/math.ts
1603
+ // src/gen/drawingml/math.ts
1591
1604
  /**
1592
- * ts-pptx: DrawingML native equations (OMML)
1605
+ * pptx-ts: DrawingML native equations (OMML)
1593
1606
  *
1594
1607
  * Wrap raw OMML markup into the `<a14:m>` marker PowerPoint uses for editable
1595
- * equations — as a standalone display paragraph (`genXmlMathParagraph`) or as an
1608
+ * equations, either as a standalone display paragraph (`genXmlMathParagraph`) or as an
1596
1609
  * inline run that flows between the surrounding `<a:r>` runs (`genXmlInlineMath`).
1597
1610
  */
1598
1611
  /** The `<a14:m>` marker, carrying both namespace declarations so the supplied OMML needs none. */
@@ -1615,7 +1628,7 @@ function a14Math(mathXml) {
1615
1628
  * @returns {string} an `<a:p>` math paragraph
1616
1629
  */
1617
1630
  function genXmlMathParagraph(omml) {
1618
- const trimmed = (omml || "").trim();
1631
+ const trimmed = omml.trim();
1619
1632
  const paraPr = el("m:oMathParaPr", null, raw(voidEl("m:jc", { "m:val": "centerGroup" })));
1620
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));
1621
1634
  }
@@ -1624,7 +1637,7 @@ function genXmlMathParagraph(omml) {
1624
1637
  *
1625
1638
  * Unlike the display form ({@link genXmlMathParagraph}), an inline equation is *not* its own
1626
1639
  * paragraph: PowerPoint authors an `<a14:m>` marker wrapping a bare `<m:oMath>` (no `<m:oMathPara>`,
1627
- * 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>`,
1628
1641
  * pinned by the `math-omml-inline.pptx` oracle. The `mc:AlternateContent` envelope stays at the
1629
1642
  * shape level (see `objectHasMath`), so nothing wraps the run itself. We declare the `a14` and `m`
1630
1643
  * namespaces on the `<a14:m>` element so the supplied OMML needs none of its own, and accept the
@@ -1635,12 +1648,11 @@ function genXmlMathParagraph(omml) {
1635
1648
  * @returns {string} an `<a14:m>` inline equation run
1636
1649
  */
1637
1650
  function genXmlInlineMath(omml) {
1638
- const trimmed = (omml || "").trim();
1651
+ const trimmed = omml.trim();
1639
1652
  const oMathMatch = trimmed.match(/<m:oMath[\s>][\s\S]*<\/m:oMath>/);
1640
1653
  return a14Math(oMathMatch ? oMathMatch[0] : el("m:oMath", null, raw(trimmed)));
1641
1654
  }
1642
- //#endregion
1643
- //#region src/gen/drawingml/text-run.ts
1655
+ // src/gen/drawingml/text-run.ts
1644
1656
  /**
1645
1657
  * `<a:extLst>` marking a hyperlink whose color should follow the text color rather than the
1646
1658
  * theme's hyperlink color. The leading spaces are byte-significant: this block was authored as
@@ -1648,7 +1660,7 @@ function genXmlInlineMath(omml) {
1648
1660
  * with `openPrefix`/`closePrefix` rather than being tidied away.
1649
1661
  */
1650
1662
  const HLINK_TEXT_COLOR_EXT = el("a:extLst", null, raw(el("a:ext", { uri: "{A12FA001-AC4F-418D-AE19-62706E023703}" }, raw(voidEl("ahyp:hlinkClr", {
1651
- "xmlns:ahyp": "http://schemas.microsoft.com/office/drawing/2018/hyperlinkcolor",
1663
+ "xmlns:ahyp": OOXML_NS.ahyp,
1652
1664
  val: "tx"
1653
1665
  }, { openPrefix: " " })), {
1654
1666
  openPrefix: " ",
@@ -1662,7 +1674,7 @@ const HLINK_TEXT_COLOR_EXT = el("a:extLst", null, raw(el("a:ext", { uri: "{A12FA
1662
1674
  *
1663
1675
  * Two call sites downstream of the emitter act on "this paragraph has a bullet" rather than on
1664
1676
  * "this paragraph says something about its bullet", and both were plain truthiness tests until
1665
- * `'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
1666
1678
  * would have broken the line grouping (every run starting its own paragraph) and eaten a real
1667
1679
  * leading `•` from text that emits no `a:buChar` to duplicate it.
1668
1680
  * @param {TextPropsOptions['bullet']} bullet - the option value
@@ -1676,12 +1688,12 @@ function emitsBulletMarkup(bullet) {
1676
1688
  *
1677
1689
  * Three inputs, because the option has three states and so does the attribute: an explicit
1678
1690
  * number is clamped and written, `'inherit'` writes nothing, and an absent option falls back to
1679
- * 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
1680
1692
  * that states nothing on its own.
1681
1693
  *
1682
1694
  * BOTH arms go through the clamp. The fallback used to be returned verbatim on the grounds that
1683
1695
  * the emitter computed it, but the emitter computes it from the caller's `bullet.indent` and
1684
- * `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
1685
1697
  * ~6700x the `ST_TextMargin` ceiling. The fallback arrives in EMU rather than points because the
1686
1698
  * bullet arms do their arithmetic there; the round trip through the clamp's own unit is exact
1687
1699
  * for any value in range, so a paragraph that was already legal emits the same bytes.
@@ -1699,7 +1711,7 @@ function resolveParagraphMargin(option, fallbackEmu, clamp) {
1699
1711
  * The paragraph's outline level, or `0` when the caller named none or named one `a:p/@lvl`
1700
1712
  * cannot hold.
1701
1713
  *
1702
- * 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
1703
1715
  * the bullet arms' default `@marL`. Validating it for one use and multiplying a margin by it in
1704
1716
  * the other is how `indentLevel: 1e6` reached `marL="342900342900"` under a warning that said
1705
1717
  * the value was being ignored.
@@ -1714,8 +1726,8 @@ function resolveIndentLevel(level) {
1714
1726
  }
1715
1727
  /**
1716
1728
  * The `@marL` a bulleted paragraph defaults to: the bullet's own hang, plus one more of it for
1717
- * every outline level. Two arms of the bullet chain write it — `bullet: {…}` and `bullet: true`
1718
- * — 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
1719
1731
  * and the other left behind.
1720
1732
  * @param {number} bulletMarLEmu - the hang one level is worth, in EMU
1721
1733
  * @param {number} indentLevel - the resolved outline level
@@ -1758,7 +1770,7 @@ function genXmlParagraphProperties(textObj, isDefault) {
1758
1770
  const bullet = opts.bullet;
1759
1771
  const bulletImage = bullet.image;
1760
1772
  const isPictureBullet = !!(bulletImage && (bulletImage.path || bulletImage.data));
1761
- if (opts.bullet?.indent) bulletMarL = ptsToEmuLenient(opts.bullet.indent);
1773
+ if (opts.bullet.indent) bulletMarL = ptsToEmuLenient(opts.bullet.indent);
1762
1774
  defaultMarL = bulletHangMarL(bulletMarL, indentLevel);
1763
1775
  defaultIndent = -bulletMarL;
1764
1776
  rejectEmptyColor(opts.bullet.color, "bullet.color");
@@ -1783,7 +1795,7 @@ function genXmlParagraphProperties(textObj, isDefault) {
1783
1795
  defaultMarL = bulletHangMarL(bulletMarL, indentLevel);
1784
1796
  defaultIndent = -bulletMarL;
1785
1797
  strXmlBullet = buCharEl(void 0, "&#x2022;", "bullet");
1786
- } else if (!opts.bullet) {
1798
+ } else {
1787
1799
  defaultMarL = 0;
1788
1800
  defaultIndent = 0;
1789
1801
  indentBeforeMarL = true;
@@ -1805,6 +1817,17 @@ function genXmlParagraphProperties(textObj, isDefault) {
1805
1817
  return paragraphPropXml;
1806
1818
  }
1807
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
+ /**
1808
1831
  * Generate XML Text Run Properties (`a:rPr`)
1809
1832
  * @param {ObjectOptions|TextPropsOptions} opts - text options
1810
1833
  * @param {boolean} isDefault - whether these are the default text run properties
@@ -1813,32 +1836,33 @@ function genXmlParagraphProperties(textObj, isDefault) {
1813
1836
  function genXmlTextRunProperties(opts, isDefault) {
1814
1837
  let runProps = "";
1815
1838
  const runPropsTag = isDefault ? "a:defRPr" : "a:rPr";
1816
- 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;
1817
- 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;
1818
1842
  const attrs = {
1819
1843
  lang: opts.lang ? opts.lang : "en-US",
1820
1844
  altLang: opts.lang ? "en-US" : null,
1821
1845
  sz: mapStated(opts.fontSize, clampFontSizeSz) ?? null,
1822
- b: xsdBoolIfTrue(opts?.bold),
1823
- i: xsdBoolIfTrue(opts?.italic),
1824
- strike: opts?.strike ? typeof opts.strike === "string" ? checkEnumOrWarn(opts.strike, TEXT_STRIKE_TYPES, "text/invalid-strike", "text `strike`") : "sngStrike" : null,
1825
- 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,
1826
1850
  u: underline,
1827
1851
  baseline,
1828
1852
  spc: mapStated(opts.charSpacing, clampCharSpacingSpc) ?? null,
1829
- kern: opts.charSpacing ? 0 : null,
1853
+ kern: opts.kerning === 0 ? 0 : mapStated(opts.kerning, clampKerningKern) ?? (opts.charSpacing ? 0 : null),
1830
1854
  dirty: "0"
1831
1855
  };
1832
1856
  const hasShadow = !!opts.shadow && opts.shadow.type !== "none";
1833
- 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) {
1834
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"))));
1835
1859
  rejectEmptyColor(opts.color, "color");
1836
1860
  if (opts.color) runProps += genXmlColorSelection(solidPaint(opts.color, opts.transparency));
1837
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]);
1838
1862
  rejectEmptyColor(opts.highlight, "highlight");
1839
1863
  if (opts.highlight) runProps += el("a:highlight", null, raw(createColorElement(opts.highlight)));
1840
- if (typeof opts.underline === "object") rejectEmptyColor(opts.underline.color, "underline.color");
1841
- 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)));
1842
1866
  if (opts.fontFace) {
1843
1867
  runProps += voidEl("a:latin", {
1844
1868
  typeface: opts.fontFace,
@@ -1847,7 +1871,7 @@ function genXmlTextRunProperties(opts, isDefault) {
1847
1871
  });
1848
1872
  if (opts.fontFaceEA) runProps += voidEl("a:ea", { typeface: opts.fontFaceEA });
1849
1873
  runProps += voidEl("a:cs", { typeface: opts.fontFace });
1850
- }
1874
+ } else if (opts.fontFaceEA) runProps += voidEl("a:ea", { typeface: opts.fontFaceEA });
1851
1875
  }
1852
1876
  if (opts.hyperlink) {
1853
1877
  try {
@@ -1855,7 +1879,12 @@ function genXmlTextRunProperties(opts, isDefault) {
1855
1879
  } catch (err) {
1856
1880
  throw new InternalError("hyperlink/not-validated", `A run reached the emitter with a hyperlink its definer should have refused: ${err.message}`);
1857
1881
  }
1858
- 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) {
1859
1888
  const link = opts.hyperlink;
1860
1889
  const linkAttrs = opts.hyperlink.url ? {
1861
1890
  "r:id": `rId${link._rId}`,
@@ -1925,7 +1954,7 @@ function groupRunsIntoLines(arrTextObjects, opts) {
1925
1954
  textObj.options.breakLine = false;
1926
1955
  }
1927
1956
  arrTexts.push(textObj);
1928
- if (arrTexts.length > 0 && textObj.options.breakLine) {
1957
+ if (textObj.options.breakLine) {
1929
1958
  if (idx + 1 < arrTextObjects.length) {
1930
1959
  arrLines.push(arrTexts);
1931
1960
  arrTexts = [];
@@ -1936,7 +1965,7 @@ function groupRunsIntoLines(arrTextObjects, opts) {
1936
1965
  return arrLines;
1937
1966
  }
1938
1967
  /**
1939
- * 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`
1940
1969
  * reads off a run's bag, in the order it reads them.
1941
1970
  *
1942
1971
  * This used to be `Object.entries(opts)` filtered on `!textOptions[key]`, which had two
@@ -1962,6 +1991,7 @@ const RUN_INHERITABLE_OPTIONS = [
1962
1991
  "subscript",
1963
1992
  "superscript",
1964
1993
  "charSpacing",
1994
+ "kerning",
1965
1995
  "color",
1966
1996
  "transparency",
1967
1997
  "fontFace",
@@ -2023,8 +2053,9 @@ const PARAGRAPH_FALSY_IS_UNSTATED = [
2023
2053
  /**
2024
2054
  * Render each grouped line to an `<a:p>` paragraph: paragraph props, inherited run options,
2025
2055
  * text runs (and inline/display math), and the closing endParaRPr.
2056
+ * @param slideColor - the colour a run marked `_slideColor` is written in
2026
2057
  */
2027
- function renderTextParagraphsXml(arrLines, slideObj, opts) {
2058
+ function renderTextParagraphsXml(arrLines, slideObj, opts, slideColor) {
2028
2059
  let strSlideXml = "";
2029
2060
  arrLines.forEach((line) => {
2030
2061
  const firstRun = line[0];
@@ -2056,6 +2087,7 @@ function renderTextParagraphsXml(arrLines, slideObj, opts) {
2056
2087
  if (key === "color" && textObj.options.hyperlink) return;
2057
2088
  setOrClear(textOptions, key, textOptions[key] ?? shapeOptions[key]);
2058
2089
  });
2090
+ setOrClear(textOptions, "_shapeHyperlink", slideObj._type !== "tablecell" && textOptions.hyperlink !== void 0 && textOptions.hyperlink === shapeOptions.hyperlink ? true : void 0);
2059
2091
  let _textRunObj = textObj;
2060
2092
  if (idx === 0 && emitsBulletMarkup(line[0]?.options.bullet) && typeof textObj.text === "string") {
2061
2093
  const _stripped = textObj.text.replace(/^[\u2022\u25E6\u25AA\u25AB\u25CF\u25CB\u2023\u2043\u2219]\s*/, "");
@@ -2066,7 +2098,16 @@ function renderTextParagraphsXml(arrLines, slideObj, opts) {
2066
2098
  }
2067
2099
  const isEmptyBreakArtifact = _textRunObj.text === "" && line.length > 1;
2068
2100
  if (_textRunObj.math) paraXml += genXmlInlineMath(_textRunObj.math);
2069
- 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
+ }
2070
2111
  if (!textObj.text && opts.fontSize || textObj.options.fontSize) {
2071
2112
  reqsClosingFontSize = true;
2072
2113
  closingFontSize = closingFontSize ?? textObj.options.fontSize;
@@ -2096,18 +2137,17 @@ function renderTextParagraphsXml(arrLines, slideObj, opts) {
2096
2137
  });
2097
2138
  return strSlideXml;
2098
2139
  }
2099
- //#endregion
2100
- //#region src/gen/drawingml/geometry.ts
2140
+ // src/gen/drawingml/geometry.ts
2101
2141
  /**
2102
- * ts-pptx: DrawingML geometry
2142
+ * pptx-ts: DrawingML geometry
2103
2143
  *
2104
2144
  * Emit `<a:prstGeom>` (preset shapes with their adjustment guides) and
2105
2145
  * `<a:custGeom>` (freeform paths built from the `points` DSL). Shared by the
2106
2146
  * shape and image code paths so path/preset emission stays in one place.
2107
2147
  */
2108
2148
  /**
2109
- * Several `<a:custGeom>` children are emitted as `<a:avLst />` — with a space before the
2110
- * 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.
2111
2151
  */
2112
2152
  const SPACE_BEFORE_SLASH = { closePrefix: " " };
2113
2153
  /**
@@ -2121,12 +2161,12 @@ const BEZ_INDENT = {
2121
2161
  };
2122
2162
  /**
2123
2163
  * The 17 guide-formula operations ECMA-376 Part 1 §20.1.9.11 (`a:gd/@fmla`) defines.
2124
- * 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
2125
2165
  * unrecognized leading token is always a caller mistake, not a newer dialect.
2126
2166
  *
2127
2167
  * Only the operation is checked. The operands are deliberately left uninterpreted:
2128
2168
  * they may be literals, adjust names, or other guide names resolved in declaration
2129
- * 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
2130
2170
  * is exactly what this passthrough exists to avoid.
2131
2171
  */
2132
2172
  const GEOM_GUIDE_OPS = /* @__PURE__ */ new Set([
@@ -2163,8 +2203,8 @@ const RECT_RADIUS_ADJ1_SHAPES = /* @__PURE__ */ new Set(["round2SameRect", "roun
2163
2203
  *
2164
2204
  * An unknown `prst` value corrupts the package: PowerPoint shows the "needs
2165
2205
  * repair" dialog and drops the shape. So this is the safety net for every
2166
- * `prstGeom` emitter — `addShape`, and the `shape` option on `addText`/`addImage`
2167
- * — 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
2168
2208
  * their own typo at the API boundary rather than at serialization time.
2169
2209
  * @param {string} shapeName - the resolved preset geometry name
2170
2210
  * @throws {InvalidOptionError} `shape/unknown-preset` when the name is not a preset
@@ -2252,8 +2292,9 @@ function isArcPoint(point) {
2252
2292
  * `<a:ahLst>`) and guide formulas (`guides` → `<a:gdLst>`) that make the shape connectable and
2253
2293
  * editable. Shared by the shape and image code paths so that path emission stays in one place.
2254
2294
  * Points and connection-site/handle positions are authored in the object's own inch/EMU space
2255
- * (0..cx, 0..cy) — not slide-relative and not normalized; a bare non-`Coord` string is passed
2256
- * 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).
2257
2298
  * @param {ObjectOptions} options - object options carrying `points`/`guides`/`connectionSites`/`adjustHandles`
2258
2299
  * @param {number} cx - object width (EMU), used as the path viewport width
2259
2300
  * @param {number} cy - object height (EMU), used as the path viewport height
@@ -2272,20 +2313,24 @@ function genXmlCustGeom(options, cx, cy, layout) {
2272
2313
  if (typeof v === "string" && /^[A-Za-z_]/.test(v.trim())) return v;
2273
2314
  return String(getSmartParseNumber(v, axis, layout));
2274
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);
2275
2320
  /** `<a:pt>` in the object's own EMU space. */
2276
2321
  const pt = (x, y) => voidEl("a:pt", {
2277
- x: getSmartParseNumber(x, "X", layout),
2278
- y: getSmartParseNumber(y, "Y", layout)
2322
+ x: pathCoord(x, "X"),
2323
+ y: pathCoord(y, "Y")
2279
2324
  }, SPACE_BEFORE_SLASH);
2280
2325
  const nodes = [];
2281
2326
  points?.forEach((point, i) => {
2282
2327
  if (isArcPoint(point)) {
2283
- 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.");
2284
2329
  nodes.push(voidEl("a:arcTo", {
2285
- hR: getSmartParseNumber(point.curve.hR, "Y", layout),
2286
- wR: getSmartParseNumber(point.curve.wR, "X", layout),
2287
- stAng: convertArcAngle(point.curve.stAng, "stAng"),
2288
- 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")
2289
2334
  }, SPACE_BEFORE_SLASH));
2290
2335
  } else if ("curve" in point) switch (point.curve.type) {
2291
2336
  case "cubic":
@@ -2379,7 +2424,7 @@ function genXmlCustGeom(options, cx, cy, layout) {
2379
2424
  ].map(raw));
2380
2425
  }
2381
2426
  /**
2382
- * `<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
2383
2428
  * that draws a plain box writes, seven of them by hand.
2384
2429
  *
2385
2430
  * `fmt` carries the caller's `openPrefix`, because indentation before this element is
@@ -2388,8 +2433,16 @@ function genXmlCustGeom(options, cx, cy, layout) {
2388
2433
  function prstGeomRect(fmt) {
2389
2434
  return el("a:prstGeom", { prst: "rect" }, raw(voidEl("a:avLst")), fmt);
2390
2435
  }
2391
- //#endregion
2392
- //#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
+ }
2393
2446
  /**
2394
2447
  * The `<p:cNvPr>` every shape renderer writes: self-closing for most shape kinds, paired around a
2395
2448
  * hyperlink or media-action child for the others.
@@ -2490,7 +2543,7 @@ function xfrmEl(tag, frame, attrs = null, fmt) {
2490
2543
  * A group's `<a:xfrm>`: the box, then an IDENTITY child coordinate space (`chOff`/`chExt` equal
2491
2544
  * to `off`/`ext`).
2492
2545
  *
2493
- * 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
2494
2547
  * group never scales its children, so a grouped shape's authored size is its rendered size.
2495
2548
  * Three group emitters spelled the four-child block out.
2496
2549
  * @param frame - the box, in EMU
@@ -2550,13 +2603,13 @@ function spTreeEl(children) {
2550
2603
  *
2551
2604
  * The child order is the point. `CT_GraphicalObjectFrame` sequences
2552
2605
  * `nvGraphicFramePr` → `xfrm` → `graphic`, and PowerPoint reports a frame that gets it wrong as
2553
- * 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
2554
2607
  * build one (chart, table, OLE, zoom, 3D model) each had to be right about separately.
2555
2608
  *
2556
2609
  * `nvGraphicFramePr` stays the caller's to build and is *not* folded in here. The five differ in
2557
- * 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
2558
2611
  * `<a:graphicFrameLocks>` default sets, a `<p:nvPr>` that is empty for three and carries a
2559
- * 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
2560
2613
  * differences is deliberate and documented where it is made.
2561
2614
  *
2562
2615
  * @param opts.nvGraphicFramePr - the already-serialized `<p:nvGraphicFramePr>`
@@ -2579,7 +2632,7 @@ function graphicFrameEl(opts) {
2579
2632
  *
2580
2633
  * Fixed rather than taken from the caller's `objectLock`, and that is true of every construct
2581
2634
  * that emits one: the fallback picture is what a consumer *without* the feature draws, while the
2582
- * 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
2583
2636
  * two element types also accept different flags, so folding a `graphicFrameLocks` set onto a
2584
2637
  * `picLocks` would warn about every flag they do not share. Routed through `genXmlObjectLock` so
2585
2638
  * attribute order comes from `PICTURE_LOCK_ATTRS` (`gen/drawingml/locks.ts`) rather than from a literal, and a flag
@@ -2607,22 +2660,21 @@ function previewPicBody(previewRid, frame, outline = false, prefix = { ns: "p" }
2607
2660
  outline ? raw(el("a:ln", { w: "3175" }, raw(el("a:solidFill", null, raw(voidEl("a:prstClr", { val: "ltGray" })))))) : null
2608
2661
  ]);
2609
2662
  }
2610
- //#endregion
2611
- //#region src/gen/opc/part-paths.ts
2663
+ // src/gen/opc/part-paths.ts
2612
2664
  /**
2613
2665
  * Where the parts of a written package live.
2614
2666
  *
2615
2667
  * Every part is named at least three times: the assembler writes it, `[Content_Types].xml`
2616
2668
  * declares an `Override` for it, and one or more `.rels` parts target it. Each of those was
2617
- * 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
2618
2670
  * name, a leading-slash part name, a relationship target resolved against the part that
2619
- * 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
2620
2672
  * PowerPoint repair, and so is a relationship pointing at a part nobody wrote.
2621
2673
  *
2622
2674
  * The canonical spelling here is the zip entry name: `ppt/slides/slide1.xml`, no leading
2623
- * 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:
2624
2676
  * {@link overrideName} for the content-types declaration, {@link targetFromPresentation} and
2625
- * {@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
2626
2678
  * and then transformed, never re-typed in a second dialect.
2627
2679
  *
2628
2680
  * Indices are 1-based throughout, matching the OPC naming rather than the arrays that drive it.
@@ -2643,7 +2695,7 @@ function slideLayoutPath(n) {
2643
2695
  }
2644
2696
  /**
2645
2697
  * The `n`-th notes-slide part. One is written for every slide, empty notes included, so the
2646
- * 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.
2647
2699
  */
2648
2700
  function notesSlidePath(n) {
2649
2701
  return `ppt/notesSlides/notesSlide${n}.xml`;
@@ -2675,14 +2727,14 @@ function overrideName(path) {
2675
2727
  return `/${path}`;
2676
2728
  }
2677
2729
  /**
2678
- * `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,
2679
2731
  * whose rels reach `slides/slide1.xml` with no traversal at all.
2680
2732
  */
2681
2733
  function targetFromPresentation(path) {
2682
2734
  return path.slice(4);
2683
2735
  }
2684
2736
  /**
2685
- * `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,
2686
2738
  * a notes slide, the master: `../slides/slide1.xml`.
2687
2739
  */
2688
2740
  function targetFromPptSubpart(path) {
@@ -2698,7 +2750,7 @@ function targetFromPptSubpart(path) {
2698
2750
  *
2699
2751
  * It exists because the slide-link target used to be spelled `slide${n}.xml` unconditionally, by
2700
2752
  * a function that also serves layouts and the master. A relationship target resolves against the
2701
- * 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
2702
2754
  * part no package has. Deriving the target from the emitting part's own path means the question
2703
2755
  * is asked once, by the only code that can answer it.
2704
2756
  */
@@ -2706,14 +2758,13 @@ function targetBetweenPptSubparts(fromPath, path) {
2706
2758
  const dir = fromPath.slice(0, fromPath.lastIndexOf("/") + 1);
2707
2759
  return path.startsWith(dir) ? path.slice(dir.length) : targetFromPptSubpart(path);
2708
2760
  }
2709
- //#endregion
2710
- //#region src/gen/opc/rels.ts
2761
+ // src/gen/opc/rels.ts
2711
2762
  /**
2712
- * ts-pptx: shared `Relationship` writers.
2763
+ * pptx-ts: shared `Relationship` writers.
2713
2764
  *
2714
2765
  * A `.rels` part is mostly one-off entries, written where the part that needs
2715
2766
  * them is built. This module holds the ones **more than one part** writes, so
2716
- * 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
2717
2768
  * `TargetMode` disagree between two writers does not throw, it produces a link
2718
2769
  * PowerPoint silently drops.
2719
2770
  */
@@ -2725,7 +2776,7 @@ function targetBetweenPptSubparts(fromPath, path) {
2725
2776
  * passed `rId3` or `3`, with seven more written inline in `gen/slide/`. The attribute set and
2726
2777
  * its order are byte-significant, so one writer is the only way they stay one fact.
2727
2778
  *
2728
- * `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`
2729
2780
  * depends on that: it computes the escaped form separately to compare a rel against the bytes
2730
2781
  * already emitted, and the two escapers must not drift.
2731
2782
  * @param {string | number} id - the relationship id; a number gets the `rId` prefix
@@ -2755,7 +2806,7 @@ function relationshipsEl(rels, fmt) {
2755
2806
  /**
2756
2807
  * A complete `.rels` part: the XML declaration, a CRLF, then {@link relationshipsEl}.
2757
2808
  *
2758
- * 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:
2759
2810
  * they follow the declaration with nothing, and two of them end with a bare newline.
2760
2811
  */
2761
2812
  function relationshipsPart(rels, fmt) {
@@ -2773,14 +2824,13 @@ function relationshipsPart(rels, fmt) {
2773
2824
  function externalHyperlinkRel(rId, target) {
2774
2825
  return relationshipEl(rId, HYPERLINK_REL, target, { targetMode: "External" });
2775
2826
  }
2776
- //#endregion
2777
- //#region src/gen/drawingml/list-style.ts
2827
+ // src/gen/drawingml/list-style.ts
2778
2828
  /**
2779
- * ts-pptx: the `<a:lvlNpPr>` list-style level and the theme-font `<a:defRPr>` inside it.
2829
+ * pptx-ts: the `<a:lvlNpPr>` list-style level and the theme-font `<a:defRPr>` inside it.
2780
2830
  *
2781
- * A deck states its text defaults in four places — the presentation's `p:defaultTextStyle`, the
2782
- * notesMaster's `p:notesStyle`, and the slide master's built-in and configured `p:txStyles` —
2783
- * 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`
2784
2834
  * over one colour, then the `latin`/`ea`/`cs` triple pointing at one theme font family. They
2785
2835
  * differed in the size, in whether `b`/`i` were carried, and in whether the colour and the
2786
2836
  * Latin face were overridable, which is what the parameters here are.
@@ -2817,8 +2867,7 @@ function lvlPPr(level, attrs, children) {
2817
2867
  ...LEVEL_PPR_TAIL
2818
2868
  }, children);
2819
2869
  }
2820
- //#endregion
2821
- //#region src/gen/slide/notes.ts
2870
+ // src/gen/slide/notes.ts
2822
2871
  /**
2823
2872
  * Collect the speaker-notes runs for a slide (flattened across any number of `addNotes()` calls).
2824
2873
  * @param {PresSlideInternal} slide - the slide object
@@ -2843,8 +2892,8 @@ function getNotesRuns(slide) {
2843
2892
  * can be written twice or gain notes between writes. Each notes-part writer computes this once and
2844
2893
  * hands the result to both the body and the rels file, which is what keeps the two in agreement.
2845
2894
  *
2846
- * Only external `url` hyperlinks are supported; any other target is left off the copy, and a
2847
- * `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.
2848
2897
  * @param {PresSlideInternal} slide - the slide object
2849
2898
  * @return {NotesSlideContent} the notes hyperlink relationships and the runs referencing them
2850
2899
  */
@@ -2856,15 +2905,7 @@ function buildNotesSlideRels(slide) {
2856
2905
  rels,
2857
2906
  runs: getNotesRuns(slide).map((run) => {
2858
2907
  const hyperlink = run.options?.hyperlink;
2859
- if (!hyperlink || !run.options) return run;
2860
- if (!hyperlink.url) {
2861
- if (hyperlink.slide) warn("notes/hyperlink-slide-unsupported", "notes hyperlinks support `url` only (ignoring `slide` target)");
2862
- const { hyperlink: _unsupported, ...options } = run.options;
2863
- return {
2864
- ...run,
2865
- options
2866
- };
2867
- }
2908
+ if (!hyperlink || !run.options || !(hyperlink.url || hyperlink.slide)) return run;
2868
2909
  lastRid++;
2869
2910
  rels.push(hyperlinkRel(lastRid, hyperlink));
2870
2911
  const linked = {
@@ -2908,8 +2949,8 @@ function genXmlNotesParagraphs(runs) {
2908
2949
  /**
2909
2950
  * PowerPoint's creation-id extension, on the `p:cSld` of both notes parts.
2910
2951
  *
2911
- * The value is a fixed literal, and that is the point of naming it: it looks like data — a
2912
- * 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
2913
2954
  * changes one of them. It is deliberately constant, which is what keeps the two notes parts
2914
2955
  * byte-identical across builds.
2915
2956
  */
@@ -3157,8 +3198,8 @@ function makeXmlNotesSlide(slide, notes) {
3157
3198
  /**
3158
3199
  * The notes slide's fixed frame, with the only two slide-dependent strings passed in.
3159
3200
  *
3160
- * A notes slide is the same three placeholders every time — the thumbnail (`sldImg`),
3161
- * 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
3162
3203
  * paragraphs and the cached slide number is a constant. It is a parameter of its own
3163
3204
  * because the read model authors notes onto a *loaded* deck (`Slide.addNotes`), where
3164
3205
  * there is no `PresSlideInternal` to serialize but the part must come out identical:
@@ -3237,7 +3278,7 @@ function makeXmlNotesSlideSkeleton(bodyParagraphsXml, slideNum) {
3237
3278
  * @return {string} XML
3238
3279
  */
3239
3280
  function makeXmlNotesSlideRel(notes, slideNumber) {
3240
- 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("");
3241
3282
  return XML_DECL + relationshipsEl([
3242
3283
  relationshipEl(1, NOTES_MASTER_REL, targetFromPptSubpart(NOTES_MASTER_PATH)),
3243
3284
  relationshipEl(2, SLIDE_REL, targetFromPptSubpart(slidePath(slideNumber))),
@@ -3258,7 +3299,95 @@ function makeXmlNotesMasterRel() {
3258
3299
  closePrefix: "\n "
3259
3300
  });
3260
3301
  }
3261
- //#endregion
3262
- 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 };
3263
3392
 
3264
- //# sourceMappingURL=notes-Dh12JAbn.js.map
3393
+ //# sourceMappingURL=comments-CKIZYRbZ.js.map