pptx-ts 3.7.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (136) hide show
  1. package/README.md +116 -269
  2. package/dist/autopage-odPz7zZO.js +536 -0
  3. package/dist/autopage-odPz7zZO.js.map +1 -0
  4. package/dist/binary-string-CKVAYI3M.js +24 -0
  5. package/dist/binary-string-CKVAYI3M.js.map +1 -0
  6. package/dist/body-insets-Dw_BeWD_.js +54 -0
  7. package/dist/body-insets-Dw_BeWD_.js.map +1 -0
  8. package/dist/browser.d.ts +29 -9
  9. package/dist/browser.d.ts.map +1 -1
  10. package/dist/browser.js +41 -14
  11. package/dist/browser.js.map +1 -1
  12. package/dist/clip-Bl01BPcN.d.ts +67 -0
  13. package/dist/clip-Bl01BPcN.d.ts.map +1 -0
  14. package/dist/diagnostics-D8ot4Ei7.d.ts +244 -0
  15. package/dist/diagnostics-D8ot4Ei7.d.ts.map +1 -0
  16. package/dist/embedded-fonts-DarT9aoa.d.ts +20 -0
  17. package/dist/embedded-fonts-DarT9aoa.d.ts.map +1 -0
  18. package/dist/entry-compose-DCnvBZz2.d.ts +1046 -0
  19. package/dist/entry-compose-DCnvBZz2.d.ts.map +1 -0
  20. package/dist/{enums-CvRYniUA.d.ts → enums-BUKTUB6V.d.ts} +21 -13
  21. package/dist/{enums-CvRYniUA.d.ts.map → enums-BUKTUB6V.d.ts.map} +1 -1
  22. package/dist/{errors-Bvln0H-7.js → errors-C4oENPcu.js} +92 -2
  23. package/dist/errors-C4oENPcu.js.map +1 -0
  24. package/dist/families.d.ts +260 -0
  25. package/dist/families.d.ts.map +1 -0
  26. package/dist/families.js +57 -0
  27. package/dist/families.js.map +1 -0
  28. package/dist/{fit-BKs0zxMI.js → fit-Cb7naFj6.js} +249 -167
  29. package/dist/fit-Cb7naFj6.js.map +1 -0
  30. package/dist/font-metrics-v5fn7s7e.d.ts +613 -0
  31. package/dist/font-metrics-v5fn7s7e.d.ts.map +1 -0
  32. package/dist/grid-DikTjFey.js +187 -0
  33. package/dist/grid-DikTjFey.js.map +1 -0
  34. package/dist/{html-dom-B0I0QSbU.js → html-dom-Iuog2wdK.js} +121 -91
  35. package/dist/html-dom-Iuog2wdK.js.map +1 -0
  36. package/dist/html.d.ts +6 -6
  37. package/dist/html.d.ts.map +1 -1
  38. package/dist/html.js +7 -6
  39. package/dist/html.js.map +1 -1
  40. package/dist/image-size-CvN2iCA4.js +1611 -0
  41. package/dist/image-size-CvN2iCA4.js.map +1 -0
  42. package/dist/index.d.ts +24 -10
  43. package/dist/index.d.ts.map +1 -1
  44. package/dist/index.js +31 -11
  45. package/dist/index.js.map +1 -1
  46. package/dist/inspect.d.ts +40 -31
  47. package/dist/inspect.d.ts.map +1 -1
  48. package/dist/inspect.js +40 -48
  49. package/dist/inspect.js.map +1 -1
  50. package/dist/math.d.ts +7 -5
  51. package/dist/math.d.ts.map +1 -1
  52. package/dist/math.js +16 -7
  53. package/dist/math.js.map +1 -1
  54. package/dist/measure.d.ts +12 -225
  55. package/dist/measure.d.ts.map +1 -1
  56. package/dist/measure.js +3 -3
  57. package/dist/node.d.ts +22 -8
  58. package/dist/node.d.ts.map +1 -1
  59. package/dist/node.js +27 -7
  60. package/dist/node.js.map +1 -1
  61. package/dist/notes-Dh12JAbn.js +3264 -0
  62. package/dist/notes-Dh12JAbn.js.map +1 -0
  63. package/dist/options-internal-WqgMf720.js +43 -0
  64. package/dist/options-internal-WqgMf720.js.map +1 -0
  65. package/dist/{package-CaOLXSNi.d.ts → package-J49Ck6S2.d.ts} +27 -12
  66. package/dist/package-J49Ck6S2.d.ts.map +1 -0
  67. package/dist/playbtn-CUqQdoDf.js +22 -0
  68. package/dist/playbtn-CUqQdoDf.js.map +1 -0
  69. package/dist/{slide-ZqAdVvHV.d.ts → pres-C6qgOFAa.d.ts} +1021 -1423
  70. package/dist/pres-C6qgOFAa.d.ts.map +1 -0
  71. package/dist/{presentation-DrJtj3-C.js → presentation-DGYlAWhl.js} +3208 -2727
  72. package/dist/presentation-DGYlAWhl.js.map +1 -0
  73. package/dist/{presentation-types-CDjScsev.d.ts → presentation-types-8jLQHI4M.d.ts} +1070 -683
  74. package/dist/presentation-types-8jLQHI4M.d.ts.map +1 -0
  75. package/dist/read.d.ts +9 -9
  76. package/dist/read.d.ts.map +1 -1
  77. package/dist/read.js +4 -4
  78. package/dist/script.d.ts +80 -66
  79. package/dist/script.d.ts.map +1 -1
  80. package/dist/script.js +2399 -1139
  81. package/dist/script.js.map +1 -1
  82. package/dist/{shapes-BemroU-z.js → shapes-re0KyQk_.js} +2807 -1609
  83. package/dist/shapes-re0KyQk_.js.map +1 -0
  84. package/dist/slide-Dx2ZeVZ6.d.ts +1256 -0
  85. package/dist/slide-Dx2ZeVZ6.d.ts.map +1 -0
  86. package/dist/slide-size-BalTjFuv.js +88 -0
  87. package/dist/slide-size-BalTjFuv.js.map +1 -0
  88. package/dist/{units-internal-Dg95w_YD.js → table-cell-margins-EVg0EwxT.js} +235 -273
  89. package/dist/table-cell-margins-EVg0EwxT.js.map +1 -0
  90. package/dist/table-dom-BTDP3lEP.js +21 -0
  91. package/dist/table-dom-BTDP3lEP.js.map +1 -0
  92. package/dist/text-KnLOtEEu.js +3980 -0
  93. package/dist/text-KnLOtEEu.js.map +1 -0
  94. package/dist/text-anchor-CrYXAfUu.js +176 -0
  95. package/dist/text-anchor-CrYXAfUu.js.map +1 -0
  96. package/dist/transition-speed-kUEWDWTx.js +14 -0
  97. package/dist/transition-speed-kUEWDWTx.js.map +1 -0
  98. package/dist/units-internal-XJlf-hOl.js +649 -0
  99. package/dist/units-internal-XJlf-hOl.js.map +1 -0
  100. package/dist/zip-C0leZZMj.d.ts +67 -0
  101. package/dist/zip-C0leZZMj.d.ts.map +1 -0
  102. package/dist/zip-CKd8ihcf.js +177 -0
  103. package/dist/zip-CKd8ihcf.js.map +1 -0
  104. package/dist/zip.d.ts +3 -68
  105. package/dist/zip.js +3 -163
  106. package/dist/zoom-cX9sllpH.js +8865 -0
  107. package/dist/zoom-cX9sllpH.js.map +1 -0
  108. package/package.json +106 -19
  109. package/skills/ts-pptx-upstream/SKILL.md +58 -57
  110. package/dist/autopage-DkjYZUFD.js +0 -423
  111. package/dist/autopage-DkjYZUFD.js.map +0 -1
  112. package/dist/diagnostics-DfBdDHre.d.ts +0 -829
  113. package/dist/diagnostics-DfBdDHre.d.ts.map +0 -1
  114. package/dist/errors-Bvln0H-7.js.map +0 -1
  115. package/dist/errors-PflMPWBn.d.ts +0 -182
  116. package/dist/errors-PflMPWBn.d.ts.map +0 -1
  117. package/dist/fit-BKs0zxMI.js.map +0 -1
  118. package/dist/html-dom-B0I0QSbU.js.map +0 -1
  119. package/dist/namespaces-BVnNu7sg.js +0 -666
  120. package/dist/namespaces-BVnNu7sg.js.map +0 -1
  121. package/dist/notes-CJFJaG6M.js +0 -1724
  122. package/dist/notes-CJFJaG6M.js.map +0 -1
  123. package/dist/package-CaOLXSNi.d.ts.map +0 -1
  124. package/dist/pres-BcBR3CSM.d.ts +0 -202
  125. package/dist/pres-BcBR3CSM.d.ts.map +0 -1
  126. package/dist/presentation-DrJtj3-C.js.map +0 -1
  127. package/dist/presentation-types-CDjScsev.d.ts.map +0 -1
  128. package/dist/shapes-BemroU-z.js.map +0 -1
  129. package/dist/slide-ZqAdVvHV.d.ts.map +0 -1
  130. package/dist/text-B8SLEaZx.js +0 -11327
  131. package/dist/text-B8SLEaZx.js.map +0 -1
  132. package/dist/units-1kRzYShk.js +0 -231
  133. package/dist/units-1kRzYShk.js.map +0 -1
  134. package/dist/units-internal-Dg95w_YD.js.map +0 -1
  135. package/dist/zip.d.ts.map +0 -1
  136. package/dist/zip.js.map +0 -1
@@ -0,0 +1,3264 @@
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
7
+ /**
8
+ * ts-pptx: turning a caller's string into something that can sit inside XML.
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
12
+ * weakest was the one reached by the least-checked input. `serializeEmbeddedFontLst` puts
13
+ * `font.typeface` straight from `pptx.embedFont({ typeface })` through an escaper that handled
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
17
+ * normalisation.
18
+ *
19
+ * The copies had a real reason: `embedded-fonts.ts` is imported by `read/api/ops/`, so it must not
20
+ * pull in `src/gen/`, and `read/oxml/dom.ts` is on the same side of that line. The answer to
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.
23
+ *
24
+ * This module imports nothing.
25
+ */
26
+ /**
27
+ * Characters XML 1.0's `Char` production excludes that a string holds in one code unit, as a
28
+ * character class: every C0 control except tab, line feed and carriage return, and the two
29
+ * non-characters U+FFFE and U+FFFF. No document may carry one in any spelling, not even as a
30
+ * character reference, and a part that does is one PowerPoint offers to repair.
31
+ *
32
+ * DEL (U+007F) is in the class too, although XML 1.0 allows it. Stripping it loses nothing a slide
33
+ * could show, and it has always been stripped.
34
+ *
35
+ * Built from `String.fromCharCode` so `no-control-regex` cannot flag it statically, and built
36
+ * ONCE: it was constructed inside the text escaper, which runs for every attribute value and
37
+ * text child in the package, and again inside `validateObjectName` under a comment saying it was
38
+ * the same set.
39
+ */
40
+ const ILLEGAL_XML_CHARS_CLASS = ((cc) => `[${cc(0)}-${cc(8)}${cc(11)}${cc(12)}${cc(14)}-${cc(31)}${cc(127)}${cc(65534)}${cc(65535)}]`)(String.fromCharCode);
41
+ /** The stripping form. Separate from {@link ILLEGAL_XML_CHARS} because a `g` regex is stateful under `.test`. */
42
+ const ILLEGAL_XML_CHARS_G = new RegExp(ILLEGAL_XML_CHARS_CLASS, "g");
43
+ /** The detecting form; see {@link ILLEGAL_XML_CHARS_G} for why the two are not one regex. */
44
+ const ILLEGAL_XML_CHARS = new RegExp(ILLEGAL_XML_CHARS_CLASS);
45
+ /** Does `value` carry a character XML 1.0 forbids? The escapers strip them; `validateObjectName` reports them. */
46
+ function hasIllegalXmlChars(value) {
47
+ return ILLEGAL_XML_CHARS.test(value);
48
+ }
49
+ /**
50
+ * Replace special XML characters with HTML-encoded strings.
51
+ *
52
+ * ELEMENT TEXT ONLY. A literal tab, carriage return or line feed is left alone here because in
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
55
+ * {@link encodeXmlAttrValue}, which every attribute-emitting path must use instead.
56
+ * @param {string | number} xml - value to encode (numbers are stringified, as callers pass counts/sizes)
57
+ * @returns {string} escaped XML
58
+ */
59
+ function encodeXmlEntities(xml) {
60
+ if (typeof xml === "undefined" || xml == null) return "";
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
+ }
63
+ /**
64
+ * Escape a value destined for an XML **attribute**.
65
+ *
66
+ * Everything {@link encodeXmlEntities} does, plus the three whitespace characters that survive in
67
+ * element text but not in an attribute: XML 1.0 section 3.3.3 requires a parser to normalise a
68
+ * literal tab, carriage return or line feed inside an attribute value to a single space *before any
69
+ * consumer sees it*. Carrying one across therefore requires a character reference, so a caller's
70
+ * `objectName: 'Abschnitts-\nuberschrift'` reads back with its line break rather than a space.
71
+ *
72
+ * Deliberately a separate function rather than a widening of `encodeXmlEntities`: that helper also
73
+ * escapes element text, where a raw newline is meaningful content emitted as-is, and escaping it
74
+ * there would change bytes across every text-bearing part in the package.
75
+ * @param {string | number} xml - value to encode (numbers are stringified, as callers pass ids/sizes)
76
+ * @returns {string} escaped XML, safe to place between attribute quotes
77
+ */
78
+ function encodeXmlAttrValue(xml) {
79
+ return encodeXmlEntities(xml).replace(/\t/g, "&#9;").replace(/\n/g, "&#10;").replace(/\r/g, "&#13;");
80
+ }
81
+ //#endregion
82
+ //#region src/gen/utils.ts
83
+ /**
84
+ * ts-pptx: shared generator helpers.
85
+ *
86
+ * The small cross-cutting pieces the OOXML writers all need and that belong to
87
+ * no single part:
88
+ * - XML text encodeXmlEntities (re-exported from `xml-escape.ts`)
89
+ * - Identifiers & naming getUuid, validateObjectName, getDuplicateObjectNames
90
+ * - Slide relationships getNewRelId, isHyperlinkRel, nextMediaTarget, preencodedPath
91
+ *
92
+ * DrawingML fragment builders moved to `gen/drawingml/{color,effect,fill,line}.ts`;
93
+ * unit conversion to `units-internal.ts` (over the public primitives in `units.ts`);
94
+ * base64/image-header decoding and media content types to `media/`.
95
+ */
96
+ /**
97
+ * Fill the `x`/`y` placeholders of a hex pattern with random nibbles.
98
+ *
99
+ * **Not replaceable by `crypto.randomUUID()`**, which exists on Node 24 and in every
100
+ * browser. This takes a *format string*, and half its callers pass a partial one:
101
+ * `gen/chart/plot-scatter.ts` asks for a bare tail (`-xxxx-xxxx-xxxx-xxxxxxxxxxxx`)
102
+ * to splice onto a padded point index, and its other two calls want a GUID shape with
103
+ * no version nibble. Only `gen/define/zoom.ts` and `presentation.ts`, which pass a
104
+ * v4-shaped pattern, could switch, and switching only those would leave two generators
105
+ * where there is now one. The analysis is recorded here so it does not get re-derived:
106
+ * the swap is low value and non-zero risk, and it is not on the roadmap.
107
+ * @link https://stackoverflow.com/questions/105034/create-guid-uuid-in-javascript#answer-2117523
108
+ * @param {string} uuidFormat - the pattern; `x` becomes a random nibble, `y` one of 8/9/a/b
109
+ * @returns {string} the filled pattern
110
+ */
111
+ function getUuid(uuidFormat) {
112
+ return uuidFormat.replace(/[xy]/g, function(c) {
113
+ const r = Math.random() * 16 | 0;
114
+ return (c === "x" ? r : r & 3 | 8).toString(16);
115
+ });
116
+ }
117
+ /**
118
+ * A fresh v4 GUID in the braced, upper-case form OOXML attributes carry:
119
+ * `{XXXXXXXX-XXXX-4XXX-YXXX-XXXXXXXXXXXX}`.
120
+ *
121
+ * That is the spelling `p14:section@id` and `zmPr@id` both want, and it was written out three
122
+ * times -- once as `zoomGuid` in `gen/define/zoom.ts` and twice inline in `presentation.ts`.
123
+ * Nothing forced the three to agree, so a fix to the pattern in one was a fix in one.
124
+ */
125
+ function bracedGuid() {
126
+ return `{${getUuid("xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx").toUpperCase()}}`;
127
+ }
128
+ /**
129
+ * Practical maximum length for a `p:cNvPr` object name. PowerPoint does not
130
+ * enforce a hard spec limit, but very long names are a strong signal of a bug
131
+ * and are unwieldy in the Selection Pane.
132
+ */
133
+ const MAX_OBJECT_NAME_LENGTH = 255;
134
+ /**
135
+ * Validate a user-supplied object name and warn (does not throw) when the value
136
+ * cannot be preserved as a stable PowerPoint Selection Pane identity. This keeps
137
+ * semantic-identity bugs visible at generation time without breaking existing
138
+ * decks that pass loose names.
139
+ * - Empty/whitespace-only names provide no usable identity.
140
+ * - Control characters are stripped by `encodeXmlEntities` when the name is
141
+ * written, so the file carries a different name than the one supplied.
142
+ * - Excessively long names may not round-trip through PowerPoint/consumers.
143
+ * @param {string} name - the raw (pre-encoding) object name
144
+ * @param {string} kind - object kind for the warning message (e.g. 'text')
145
+ * @returns {string} the name unchanged (validation only)
146
+ */
147
+ function validateObjectName(name, kind) {
148
+ if (typeof name !== "string") return name;
149
+ if (name.trim().length === 0) {
150
+ warn("object-name/empty", `${kind} objectName is empty or whitespace-only; it will not provide a stable Selection Pane identity.`);
151
+ return name;
152
+ }
153
+ if (hasIllegalXmlChars(name)) warn("object-name/control-characters", `${kind} objectName "${name}" contains characters XML 1.0 forbids (control characters, or U+FFFE or U+FFFF) that will be stripped from the name written to the file.`);
154
+ if (name.length > MAX_OBJECT_NAME_LENGTH) warn("object-name/too-long", `${kind} objectName exceeds ${MAX_OBJECT_NAME_LENGTH} characters and may not be preserved by PowerPoint.`);
155
+ return name;
156
+ }
157
+ /**
158
+ * Return object names that appear more than once in the given list. Used to warn
159
+ * when duplicate Selection Pane identities would be emitted on a single slide,
160
+ * which breaks consumers (e.g. semantic manifests) that rely on unique names.
161
+ * @param {string[]} names - object names emitted on one slide
162
+ * @returns {string[]} the duplicated names (each listed once)
163
+ */
164
+ function getDuplicateObjectNames(names) {
165
+ const seen = /* @__PURE__ */ new Set();
166
+ const dupes = /* @__PURE__ */ new Set();
167
+ names.forEach((name) => {
168
+ if (typeof name !== "string" || name.length === 0) return;
169
+ if (seen.has(name)) dupes.add(name);
170
+ else seen.add(name);
171
+ });
172
+ return Array.from(dupes);
173
+ }
174
+ /**
175
+ * Get a new rel ID (rId) for charts, media, etc.
176
+ *
177
+ * Counting is the normal case: a slide's rels are minted in order, so one past the
178
+ * 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
180
+ * row's hyperlink on each overflow slide under the id stamped on the shared
181
+ * hyperlink object (`gen/define/hyperlinks.ts`). That id is then held but not
182
+ * 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
184
+ * resolves such a reference to whichever comes first. Hence the scan: start where
185
+ * the count says, then step over anything the slide already holds.
186
+ * @param {PresSlideInternal} target - the slide to use
187
+ * @returns {number} the lowest free rId at or above the current rel count plus 1
188
+ */
189
+ function getNewRelId(target) {
190
+ const held = heldRelIds(target);
191
+ let rId = target._rels.length + target._relsChart.length + target._relsMedia.length + 1;
192
+ while (held.has(rId)) rId++;
193
+ return rId;
194
+ }
195
+ /**
196
+ * Every rel id a slide already holds, across its hyperlink, chart and media rels.
197
+ * @param {PresSlideInternal} target - the slide to use
198
+ * @returns {Set<number>} the held ids
199
+ */
200
+ function heldRelIds(target) {
201
+ const held = /* @__PURE__ */ new Set();
202
+ target._rels.forEach((rel) => held.add(rel.rId));
203
+ target._relsChart.forEach((rel) => held.add(rel.rId));
204
+ target._relsMedia.forEach((rel) => held.add(rel.rId));
205
+ return held;
206
+ }
207
+ /**
208
+ * Whether a slide relationship is a hyperlink (external URL or internal slide
209
+ * link). The relationship `type` is stringly-typed (`'hyperlink'`, `'online'`,
210
+ * mixed-case variants), so this centralizes the case-insensitive predicate that
211
+ * was duplicated across the slide-rels writer (`gen/slide/slide.ts`) and the inspect path
212
+ * (presentation.ts). For an internal slide-to-slide link, `rel.data === 'slide'` and
213
+ * `rel.Target` is the 1-based target slide number.
214
+ * @param {{ type: string }} rel - a slide relationship
215
+ * @returns {boolean} true if the rel is any kind of hyperlink
216
+ */
217
+ function isHyperlinkRel(rel) {
218
+ return rel.type.toLowerCase().includes("hyperlink");
219
+ }
220
+ /**
221
+ * The slide-scoped segment of a media part's name.
222
+ *
223
+ * Media targets are namespaced by the slide that registered them so that slide
224
+ * master (`sm`) and slide layout (`sl-N`) media never collide with regular slide
225
+ * media names in large decks. A layout is identified by the `_slideNum >= 1000`
226
+ * convention the layout builder stamps on it; a master has no `_slideNum` at all.
227
+ * @param {PresSlideInternal} target - the slide, layout or master registering the media
228
+ * @returns {string} the key to splice into the media part name
229
+ */
230
+ function mediaSlideKey(target) {
231
+ if (target._slideNum == null) return "sm";
232
+ return target._slideNum >= 1e3 ? `sl-${target._slideNum}` : `${target._slideNum}`;
233
+ }
234
+ /**
235
+ * The target of the media part the NEXT push onto `target._relsMedia` will create:
236
+ * `../media/image-3-2.png`, or `../embeddings/oleObject-sm-1.xlsx` for a `dir` of
237
+ * `embeddings`.
238
+ *
239
+ * 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,
241
+ * not hoisted above an earlier push, which would rename every part after it. Eight sites
242
+ * spelled the rule out by hand with `_relsMedia.length + 1` inline; naming it is what lets
243
+ * the off-by-one convention be stated once and read.
244
+ *
245
+ * @param target - the slide, layout or master registering the media
246
+ * @param kind - the name's leading segment: `image`, `media`, `model3d`, `audio`, `oleObject`
247
+ * @param extn - the part's file extension, without the dot
248
+ * @param dir - which sibling directory the part lands in
249
+ */
250
+ function nextMediaTarget(target, kind, extn, dir = "media") {
251
+ return `../${dir}/${kind}-${mediaSlideKey(target)}-${target._relsMedia.length + 1}.${extn}`;
252
+ }
253
+ /**
254
+ * 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
256
+ * with {@link isPreencodedPath}.
257
+ *
258
+ * The spelling still reaches a deck. An image object with no alt text falls back to its path for
259
+ * the shape's description, so an inline image is described as `preencoded.png`, and changing
260
+ * this changes those bytes.
261
+ *
262
+ * There were four spellings of it, one with no dot, and two SVG registrations that put the
263
+ * base64 payload itself in `path`. They worked only because the media pass skipped any path
264
+ * *containing* the word.
265
+ * @param extn - the part's file extension, without the dot
266
+ */
267
+ function preencodedPath(extn) {
268
+ return `preencoded.${extn}`;
269
+ }
270
+ /** Whether `path` is a {@link preencodedPath} placeholder rather than a file or URL to load. */
271
+ function isPreencodedPath(path) {
272
+ return path.startsWith("preencoded.");
273
+ }
274
+ //#endregion
275
+ //#region src/ooxml/xml-decl.ts
276
+ /**
277
+ * The XML prolog every OOXML part this library writes begins with, including the parts the read
278
+ * side regenerates when it serializes a package it has changed.
279
+ *
280
+ * It lived with the generator-only constants, out of the read side's reach, so the read side's
281
+ * serializers spelled it out again four times. A prolog that differs between two parts by one
282
+ * attribute is not something any check reports, which is why it has one home that both sides can
283
+ * import: this module imports nothing, and no `ooxml/` module imports from `gen/` or `read/`.
284
+ */
285
+ const XML_DECL = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>";
286
+ //#endregion
287
+ //#region src/embedded-fonts.ts
288
+ /**
289
+ * Shared model + serializer for PresentationML embedded fonts, used by both the
290
+ * write side (author-side `pptx.embedFont`, `gen/pres/presentation.ts`) and the read side
291
+ * (the `embedFonts` carry in `importSlide`, `importSlides`, `importSlideMasters` and
292
+ * `appendSlides`, `src/read/api/ops/embedded-fonts.ts`).
293
+ *
294
+ * Embedded fonts are three coordinated pieces (ECMA-376 transitional):
295
+ * 1. binary font parts `/ppt/fonts/fontN.fntdata` — the **raw** TTF/OTF bytes
296
+ * (PresentationML does not obfuscate embedded fonts), content type
297
+ * `application/x-fontdata` via a single `fntdata` Default extension;
298
+ * 2. one `font` relationship per face from `presentation.xml`;
299
+ * 3. a `p:embeddedFontLst` in `presentation.xml`, at index 7 of the
300
+ * `CT_Presentation` child sequence (after `smartTags`, before `custShowLst`
301
+ * … `defaultTextStyle`).
302
+ *
303
+ * This module owns only the OOXML-shape knowledge; rId allocation and part
304
+ * placement stay with each caller (their packaging models differ).
305
+ *
306
+ * It must not import from `src/gen/`, because `read/api/ops/embedded-fonts.ts` imports it. What
307
+ * it needs from elsewhere -- the attribute escaper, the `font` relationship type and the face slot
308
+ * list -- comes from modules that import nothing outside `src/ooxml/` themselves.
309
+ */
310
+ /** Extension for the binary font parts (`/ppt/fonts/fontN.fntdata`). */
311
+ const FONT_DATA_EXTENSION = "fntdata";
312
+ /** Content type for a `.fntdata` part — one `Default` covers every font part. */
313
+ const FONT_DATA_CONTENT_TYPE = "application/x-fontdata";
314
+ /** Relationship type from `presentation.xml` to a font part (one per face). */
315
+ const FONT_REL_TYPE = FONT_REL;
316
+ /**
317
+ * Flatten `fonts` into one ordered face list, assigning sequential part indices
318
+ * (1-based) and relationship ids (starting at `firstRId`). Faces are emitted in
319
+ * `fonts` order, each font's faces in {@link EMBEDDED_FONT_SLOTS} order. Faces
320
+ * without `bytes` are skipped (nothing to write).
321
+ */
322
+ function flattenEmbeddedFaces(fonts, firstRId) {
323
+ const flat = [];
324
+ let partIndex = 0;
325
+ let rId = firstRId;
326
+ fonts.forEach((font, fontIndex) => {
327
+ for (const slot of EMBEDDED_FONT_SLOTS) {
328
+ const face = font.faces.find((f) => f.slot === slot);
329
+ if (!face || !face.bytes) continue;
330
+ flat.push({
331
+ fontIndex,
332
+ slot,
333
+ bytes: face.bytes,
334
+ partIndex: ++partIndex,
335
+ rId: rId++
336
+ });
337
+ }
338
+ });
339
+ return flat;
340
+ }
341
+ /**
342
+ * Serialize a `<p:embeddedFontLst>` from `fonts` and a `(fontIndex, slot) → rId`
343
+ * lookup (typically backed by {@link flattenEmbeddedFaces}). Returns `''` when no
344
+ * font has any face with an allocated rId, so callers can emit nothing. Assumes
345
+ * the enclosing document declares the `p:` and `r:` prefixes (presentation.xml
346
+ * does), so no namespace declarations are emitted here.
347
+ */
348
+ function serializeEmbeddedFontLst(fonts, rIdForFace) {
349
+ const entries = [];
350
+ fonts.forEach((font, fontIndex) => {
351
+ const faceXml = [];
352
+ for (const slot of EMBEDDED_FONT_SLOTS) {
353
+ const rId = rIdForFace(fontIndex, slot);
354
+ if (rId === void 0) continue;
355
+ faceXml.push(`<p:${slot} r:id="rId${rId}"/>`);
356
+ }
357
+ if (faceXml.length === 0) return;
358
+ let fontAttrs = `typeface="${encodeXmlAttrValue(font.typeface)}"`;
359
+ if (font.panose !== void 0) fontAttrs += ` panose="${encodeXmlAttrValue(font.panose)}"`;
360
+ if (font.pitchFamily !== void 0) fontAttrs += ` pitchFamily="${font.pitchFamily}"`;
361
+ if (font.charset !== void 0) fontAttrs += ` charset="${font.charset}"`;
362
+ entries.push(`<p:embeddedFont><p:font ${fontAttrs}/>${faceXml.join("")}</p:embeddedFont>`);
363
+ });
364
+ if (entries.length === 0) return "";
365
+ return `<p:embeddedFontLst>${entries.join("")}</p:embeddedFontLst>`;
366
+ }
367
+ //#endregion
368
+ //#region src/gen/oxml/el.ts
369
+ /**
370
+ * ts-pptx: XML element builder (write-side substrate)
371
+ *
372
+ * The write path historically concatenated template strings and called
373
+ * `encodeXmlEntities` by hand at every interpolation — the source of escaping,
374
+ * attribute-order and child-sequence bugs. This builder centralizes escaping
375
+ * while reproducing today's exact byte layout, so a migration onto it can be
376
+ * gated on strict byte-identity (`pnpm run byte-identity:check`) rather than on
377
+ * looser schema equivalence.
378
+ *
379
+ * Mirrors `src/read/oxml/dom.ts` on the write side.
380
+ *
381
+ * Two rules make byte-identity achievable:
382
+ *
383
+ * 1. Self-closing is decided by ARITY, never by child value. `el()` always emits
384
+ * a paired tag; `voidEl()` always self-closes. This matters because
385
+ * `encodeXmlEntities(undefined)` returns `''`, so a value-based rule would
386
+ * turn today's `<dc:title></dc:title>` into `<dc:title/>` — a silent byte
387
+ * regression on every optional-text element in the tree.
388
+ *
389
+ * Escaping is split to match: attribute values go through `encodeXmlAttrValue`
390
+ * (which adds the `&#9;`/`&#10;`/`&#13;` character references an XML parser
391
+ * would otherwise normalise to a space), text children through
392
+ * `encodeXmlEntities`, where those same characters are content.
393
+ *
394
+ * 2. Whitespace is explicit, per element, via `fmt`. Most emitted parts are flat
395
+ * (no `fmt` needed at all); the pretty-printed ones carry indentation that is
396
+ * not always depth-regular, so it is described rather than derived. Quirks
397
+ * like a misindented closing tag stay visible in the call site instead of
398
+ * being hidden inside a template literal.
399
+ */
400
+ /** Wrap already-serialized XML so it is interpolated verbatim. */
401
+ function raw(xml) {
402
+ return { raw: xml };
403
+ }
404
+ function isRaw(child) {
405
+ return typeof child === "object" && child !== null && typeof child.raw === "string";
406
+ }
407
+ function openTag(name, attrs, fmt) {
408
+ let out = (fmt?.openPrefix ?? "") + "<" + name;
409
+ for (const [key, value] of Object.entries(attrs ?? {})) {
410
+ if (value === void 0 || value === null) continue;
411
+ out += " " + key + "=\"" + encodeXmlAttrValue(value) + "\"";
412
+ }
413
+ return out;
414
+ }
415
+ /**
416
+ * Paired element: `<name …>children</name>` — emitted even when `children` is
417
+ * empty or `undefined`. Text children are escaped; `raw()` children are not.
418
+ */
419
+ function el(name, attrs, children, fmt) {
420
+ let out = openTag(name, attrs, fmt) + ">";
421
+ const childPrefix = fmt?.childPrefix ?? "";
422
+ for (const child of Array.isArray(children) ? children : [children]) {
423
+ if (child === void 0 || child === null) continue;
424
+ out += childPrefix + (isRaw(child) ? child.raw : encodeXmlEntities(child));
425
+ }
426
+ return out + (fmt?.closePrefix ?? "") + "</" + name + ">";
427
+ }
428
+ /**
429
+ * Self-closing element: `<name …/>`. `fmt.closePrefix` goes before the `/>` — a
430
+ * handful of emitters write `<a:avLst />` with a space there, and that space is
431
+ * byte-significant like any other.
432
+ */
433
+ function voidEl(name, attrs, fmt) {
434
+ return openTag(name, attrs, fmt) + (fmt?.closePrefix ?? "") + "/>";
435
+ }
436
+ /**
437
+ * Paired around `childXml` when it holds anything, self-closing when it does not.
438
+ *
439
+ * The one place arity follows content, and on purpose: a lock set, a binding list or an effect
440
+ * that may be empty makes its element paired or self-closing, and PowerPoint writes it that way.
441
+ * Rule 1 above still holds everywhere else. Each caller used to spell the choice as its own
442
+ * `x ? el(n, a, raw(x)) : voidEl(n, a)`.
443
+ * @param childXml - already-serialized children; empty or absent means self-closing
444
+ */
445
+ function elOrVoid(name, attrs, childXml, fmt) {
446
+ return childXml ? el(name, attrs, raw(childXml), fmt) : voidEl(name, attrs, fmt);
447
+ }
448
+ //#endregion
449
+ //#region src/gen/drawingml/color.ts
450
+ /**
451
+ * ts-pptx: DrawingML color elements
452
+ *
453
+ * Emit the bare `<a:srgbClr>` / `<a:schemeClr>` color element that every other
454
+ * color context builds on (solid fills, gradient stops, effect alphas, line
455
+ * fills, highlights, patterns).
456
+ *
457
+ * The RGB->hex helpers this module used to carry now live in
458
+ * `gen/table/html-dom.ts`, their only caller: they are reachable exclusively from
459
+ * the browser-only `tableToSlides` path, so keeping them here made them permanent
460
+ * dead weight in the Node chunk's coverage report (33% functions on a file that is
461
+ * otherwise fully exercised) while html-dom.ts is coverage-excluded wholesale.
462
+ */
463
+ /**
464
+ * Opening delimiter of an `<a:alpha>` child, for detecting one a caller already
465
+ * supplied. A prefix rather than a whole element: the value is the caller's, and
466
+ * this asks only whether the element is present.
467
+ */
468
+ const ALPHA_TAG = "<a:alpha";
469
+ /**
470
+ * Emit a bare DrawingML color element — either `<a:schemeClr>` (scheme color) or `<a:srgbClr>`
471
+ * (hex RGB). This is a low-level primitive: it is the shared building block for every color
472
+ * context (solid fills, gradient stops, `<a:alpha>` on shadows/glow, line fills, highlight,
473
+ * underline, patterns, …), so it is intentionally NOT limited to solid fills and cannot be
474
+ * folded into `genXmlColorSelection`.
475
+ *
476
+ * When you specifically want a *solid fill*, prefer `genXmlColorSelection` (`fill.ts`), which
477
+ * 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
479
+ * need the raw color element (e.g. inside `<a:ln>`, `<a:gs>`, an effect, or a highlight).
480
+ * @param {string|SCHEME_COLORS} colorStr - hex RGB (e.g. "FFFF00") or a scheme color constant (e.g. SchemeColor.ACCENT1)
481
+ * @param {string} [innerElements] - additional elements that adjust the color, nested inside the color element
482
+ * @returns {string} XML string
483
+ */
484
+ function createColorElement(colorStr, innerElements) {
485
+ if (typeof colorStr !== "string") {
486
+ warn("color/not-a-string", `createColorElement: expected a string color value, got ${typeof colorStr}. "${DEF_FONT_COLOR}" used instead.`);
487
+ colorStr = DEF_FONT_COLOR;
488
+ }
489
+ if (colorStr === "") {
490
+ 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
+ colorStr = DEF_FONT_COLOR;
492
+ }
493
+ let colorVal = stripHash(colorStr || "");
494
+ const rgba = splitRgbaHex(colorVal);
495
+ if (rgba.alpha !== void 0) {
496
+ if (!innerElements?.includes(ALPHA_TAG)) innerElements = alphaEl(Math.round(rgba.alpha * PERCENT_SCALE)) + (innerElements || "");
497
+ colorVal = rgba.rgb;
498
+ }
499
+ if (!isColorValue(colorVal)) {
500
+ warn("color/invalid-value", `"${colorVal}" is not a valid scheme color or hex RGB! "${DEF_FONT_COLOR}" used instead. Only provide 6-digit RGB or 'SchemeColor' values!`);
501
+ colorVal = DEF_FONT_COLOR;
502
+ }
503
+ const isHex = isHexColor(colorVal);
504
+ return elOrVoid(isHex ? "a:srgbClr" : "a:schemeClr", { val: isHex ? colorVal.toUpperCase() : colorVal }, innerElements);
505
+ }
506
+ /**
507
+ * Whether {@link createColorElement} paints `value` as given rather than replacing it: 6-digit or
508
+ * 8-digit (RGBA) hex, with or without a leading `#`, or a {@link SchemeColor} name.
509
+ *
510
+ * A normalizer that wants to fall back to its own default instead of the emitter's black asks
511
+ * this, so the two cannot disagree about what a colour is.
512
+ * @param value - a colour option's value, whatever the caller passed
513
+ */
514
+ function isColorValue(value) {
515
+ if (typeof value !== "string") return false;
516
+ const { rgb } = splitRgbaHex(value);
517
+ return isHexColor(rgb) || Object.values(SchemeColor).includes(rgb);
518
+ }
519
+ /**
520
+ * Reject an empty colour string, reporting it once per option that named one.
521
+ *
522
+ * `''` is not a spelling of "no paint": omission already spells that, and `'inherit'` and
523
+ * `'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
526
+ * have. What it must not do is coerce to {@link DEF_FONT_COLOR}, which paints visible black
527
+ * on a shape the caller expected to keep the theme's paint.
528
+ *
529
+ * Every site that consumes a caller-supplied colour calls this *before* its own fallback, so
530
+ * the diagnostic names the option that carried the empty string rather than whichever
531
+ * emitter happened to see it last. {@link createColorElement} is the exception and reports
532
+ * the same code itself: its slot has no absent state to fall back to.
533
+ * @param value - a colour option's value, whatever shape the option accepts
534
+ * @param option - the option's name, for the message
535
+ * @returns true when `value` was the empty string
536
+ */
537
+ function rejectEmptyColor(value, option) {
538
+ 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.`);
540
+ return true;
541
+ }
542
+ /**
543
+ * The colour the caller named for `option`, or `fallback` when they named none.
544
+ *
545
+ * The `x ?? DEFAULT` and `x || DEFAULT` this replaces disagreed about `''`: the first passed
546
+ * 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
548
+ * way an absent option is.
549
+ * @param value - the caller's colour option, possibly absent or empty
550
+ * @param fallback - what an absent option resolves to at this site
551
+ * @param option - the option's name, for the message
552
+ */
553
+ function namedColorOr(value, fallback, option) {
554
+ if (value === void 0 || rejectEmptyColor(value, option)) return fallback;
555
+ return value;
556
+ }
557
+ /**
558
+ * The `<a:alpha val>` child a colour element carries.
559
+ *
560
+ * Four sites built it: the RGBA split above, the gradient stop's `transparency`, the glow and
561
+ * shadow `opacity`, and the chart's `chartColorsOpacity`. Each converts to the fixed
562
+ * percentage its own option speaks first; this is only the element.
563
+ * @param fixedPct - the alpha in `ST_PositiveFixedPercentage` units (`100%` is `100000`)
564
+ */
565
+ function alphaEl(fixedPct) {
566
+ return voidEl("a:alpha", { val: fixedPct });
567
+ }
568
+ //#endregion
569
+ //#region src/gen/drawingml/effect.ts
570
+ /**
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
690
+ *
691
+ * A leaf module on purpose. A picture object (`addImage`) and a native picture fill
692
+ * (`fill: { type: 'image' }`) crop a source image the same way, but they live on opposite
693
+ * sides of a bundling boundary: `gen/drawingml/fill.ts` is reached from every shape, table
694
+ * and chart emitter, while `gen/drawingml/image.ts` pulls the image-format sniffing tables
695
+ * in behind it through `media/image-size.ts`. Sharing the crop from here rather than from
696
+ * `image.ts` is what keeps ~24 kB of format headers out of the `read` and `inspect` entry
697
+ * points, which never measure an image.
698
+ */
699
+ /**
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.
703
+ * @param crop - edge insets in percent; omitted edges default to 0
704
+ * @param label - the option's owner, as an error message names it (`addImage`, `image fill`)
705
+ * @param where - trailing context for an error message, or `''`
706
+ */
707
+ function normalizeImageCrop(crop, label, where) {
708
+ const edges = {
709
+ l: crop.l ?? 0,
710
+ t: crop.t ?? 0,
711
+ r: crop.r ?? 0,
712
+ b: crop.b ?? 0
713
+ };
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}.`);
715
+ 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
+ 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
+ return edges;
718
+ }
719
+ /**
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.
725
+ * @param crop - edge insets in percent; omitted edges default to 0
726
+ * @param label - the option's owner, as an error message names it
727
+ * @param where - trailing context for an error message, or `''`
728
+ * @returns the `<a:srcRect>` element on its own; the caller supplies the fill directive
729
+ */
730
+ function genXmlImageCropRect(crop, label, where) {
731
+ const edges = normalizeImageCrop(crop, label, where);
732
+ const v = (perc) => Math.round(perc * FIXED_PCT_PER_PERCENT);
733
+ return voidEl("a:srcRect", {
734
+ l: v(edges.l),
735
+ t: v(edges.t),
736
+ r: v(edges.r),
737
+ b: v(edges.b)
738
+ });
739
+ }
740
+ /**
741
+ * `<a:stretch><a:fillRect/></a:stretch>` — the blipFill child that scales the source to the
742
+ * shape's box, which every picture-bearing emitter writes and none parameterizes.
743
+ *
744
+ * After an `<a:srcRect/>`, it completes the canonical picture fill ECMA-376 gives
745
+ * (§L.4.8.4.3). A picture's placement is split across two elements: the source rectangle and
746
+ * this directive, which carry `sizing` and `crop`, sit in `p:blipFill`, while the clip
747
+ * geometry (`shape`, `points`, `rounding`) sits in `p:spPr`.
748
+ *
749
+ * Here rather than in `image.ts` for the same bundling reason the crop is: `fill.ts` reaches
750
+ * it from every shape, table and chart emitter, and must not pull the image-format tables in.
751
+ */
752
+ const STRETCH_FILL_RECT = el("a:stretch", null, raw(voidEl("a:fillRect")));
753
+ //#endregion
754
+ //#region src/gen/drawingml/fill.ts
755
+ /**
756
+ * ts-pptx: DrawingML fills
757
+ *
758
+ * Emit the fill group a shape, cell, chart element, or line paint can carry:
759
+ * `<a:solidFill>`, `<a:gradFill>`, `<a:pattFill>`, `<a:blipFill>`.
760
+ * {@link genXmlColorSelection} is the dispatch every caller should reach for;
761
+ * the per-kind builders are exported for the few sites that know their fill kind.
762
+ */
763
+ function normalizeGradientAngle(angle) {
764
+ const degrees = angle ?? 0;
765
+ if (typeof degrees !== "number" || !Number.isFinite(degrees)) throw new InvalidOptionError("gradient/angle-non-finite", "Gradient angle must be a finite number.");
766
+ return convertRotationDegrees((degrees % 360 + 360) % 360);
767
+ }
768
+ function alphaFromTransparency(transparency) {
769
+ return mapStated(transparency, (value) => alphaEl(transparencyToAlpha(value))) ?? "";
770
+ }
771
+ function normalizeGradientStops(stops) {
772
+ if (!Array.isArray(stops) || stops.length < 2) throw new InvalidOptionError("gradient/too-few-stops", "Gradient fill requires at least two stops.");
773
+ return stops.map((stop) => {
774
+ if (!stop || typeof stop.position !== "number" || !Number.isFinite(stop.position)) throw new InvalidOptionError("gradient/stop-position-non-finite", "Gradient stop position must be a finite number from 0 to 100.");
775
+ if (stop.position < 0 || stop.position > 100) throw new InvalidOptionError("gradient/stop-position-out-of-range", "Gradient stop position must be from 0 to 100.");
776
+ return stop;
777
+ }).sort((a, b) => a.position - b.position);
778
+ }
779
+ /**
780
+ * Create a native DrawingML gradient fill.
781
+ * @param {GradientFillProps} gradient gradient fill options
782
+ * @returns XML string
783
+ */
784
+ function genXmlGradientFill(gradient) {
785
+ if (!gradient || gradient.kind !== "linear" && gradient.kind !== "radial") throw new UnsupportedFeatureError("gradient/type-unsupported", "Gradient fill currently supports only linear and radial gradients.");
786
+ if (typeof gradient.rotateWithShape !== "undefined" && typeof gradient.rotateWithShape !== "boolean") throw new InvalidOptionError("gradient/rotate-with-shape-not-boolean", "Gradient rotateWithShape must be a boolean.");
787
+ const stops = normalizeGradientStops(gradient.stops);
788
+ const rotWithShape = gradient.rotateWithShape ?? true;
789
+ const gsLst = el("a:gsLst", null, stops.map((stop) => raw(el("a:gs", { pos: Math.round(stop.position * FIXED_PCT_PER_PERCENT) }, raw(createColorElement(stop.color, alphaFromTransparency(stop.transparency)))))));
790
+ let shape;
791
+ if (gradient.kind === "radial") {
792
+ const centerPct = (value, axis) => clampRangedInput(value ?? 50, 0, 100, "gradient/center-out-of-range", `gradient.center.${axis}`);
793
+ const cx = centerPct(gradient.center?.x, "x");
794
+ const cy = centerPct(gradient.center?.y, "y");
795
+ shape = el("a:path", { path: "circle" }, raw(voidEl("a:fillToRect", {
796
+ l: Math.round(cx * FIXED_PCT_PER_PERCENT),
797
+ t: Math.round(cy * FIXED_PCT_PER_PERCENT),
798
+ r: Math.round((100 - cx) * FIXED_PCT_PER_PERCENT),
799
+ b: Math.round((100 - cy) * FIXED_PCT_PER_PERCENT)
800
+ })));
801
+ } else {
802
+ if (typeof gradient.scaled !== "undefined" && typeof gradient.scaled !== "boolean") throw new InvalidOptionError("gradient/scaled-not-boolean", "Gradient scaled must be a boolean.");
803
+ shape = voidEl("a:lin", {
804
+ ang: normalizeGradientAngle(gradient.angle),
805
+ scaled: typeof gradient.scaled === "boolean" ? xsdBool(gradient.scaled) : void 0
806
+ });
807
+ }
808
+ return el("a:gradFill", { rotWithShape: xsdBool(rotWithShape) }, [raw(gsLst), raw(shape)]);
809
+ }
810
+ /**
811
+ * Create a native DrawingML pattern fill.
812
+ * @param {PatternFillProps} pattern pattern fill options
813
+ * @returns XML string
814
+ */
815
+ function genXmlPatternFill(pattern) {
816
+ if (!pattern) throw new InvalidOptionError("pattern-fill/missing-pattern", "Pattern fill requires a pattern object.");
817
+ const fgColor = namedColorOr(pattern.fgColor, "000000", "pattern.fgColor");
818
+ 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))))]);
820
+ }
821
+ /**
822
+ * Create a native DrawingML picture (image) fill.
823
+ * The media relationship is registered when the object is added; this only emits
824
+ * the `<a:blipFill>` referencing the pre-resolved rId.
825
+ * @param props fill props (must carry a resolved `_imgRid`)
826
+ * @returns XML string
827
+ */
828
+ function genXmlImageFill(props) {
829
+ if (!props || typeof props._imgRid !== "number") {
830
+ warn("image-fill/unresolved-media", "image fill is missing its resolved media reference; falling back to no fill. Provide `image: { path }` or `image: { data }`.");
831
+ return voidEl("a:noFill");
832
+ }
833
+ const alpha = props.transparency;
834
+ const blipInner = alpha ? voidEl("a:alphaModFix", { amt: transparencyToAlpha(alpha) }) : "";
835
+ const srcRect = props.image?.crop ? genXmlImageCropRect(props.image.crop, "image fill", "") : voidEl("a:srcRect");
836
+ return el("a:blipFill", {
837
+ dpi: 0,
838
+ rotWithShape: 1
839
+ }, [
840
+ raw(el("a:blip", { "r:embed": `rId${props._imgRid}` }, raw(blipInner))),
841
+ raw(srcRect),
842
+ raw(STRETCH_FILL_RECT)
843
+ ]);
844
+ }
845
+ /**
846
+ * Resolve which fill kind a set of fill or line props asks for.
847
+ *
848
+ * **An explicit `type` always wins**, even against a sub-object that disagrees:
849
+ * `{ type: 'solid', gradient }` is a solid fill whose gradient is ignored. That is the rule
850
+ * `type: 'none'` needs in order to mean anything (`{ type: 'none', color }` must stay
851
+ * transparent), so it is the rule everywhere rather than one kind's exception.
852
+ *
853
+ * With no `type`, the first sub-object present selects the kind, in declaration order —
854
+ * `gradient`, then `pattern`, then `image`. Setting two without a `type` is already a
855
+ * contradiction; the order exists so the answer is stated rather than emergent.
856
+ *
857
+ * Everything else is `'solid'`, which is what `type` documents as its default.
858
+ *
859
+ * This is the one answer. It used to be given in four places: the stroke emitter inferred
860
+ * `gradient` only, the shared dispatcher inferred nothing at all (so `fill: { gradient }`
861
+ * painted a black `<a:solidFill>` and blamed the caller's colours for it), and three
862
+ * `define/` modules each carried their own copy of the image half.
863
+ */
864
+ function resolveFillKind(props) {
865
+ if (!props || typeof props === "string") return "solid";
866
+ if (props.type) return props.type;
867
+ if (props.gradient) return "gradient";
868
+ if (props.pattern) return "pattern";
869
+ if ("image" in props && props.image) return "image";
870
+ return "solid";
871
+ }
872
+ /**
873
+ * {@link resolveFillKind} for a stroke, which has one kind fewer.
874
+ *
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
877
+ * and `ShapeLineProps` subtracts `image` from the fill props it inherits. This is where the
878
+ * subtraction is enforced at run time, for the JS caller TypeScript cannot stop: the request
879
+ * is refused rather than painted as nothing, because emitting a blipFill inside `a:ln` is a
880
+ * package PowerPoint reports as needing repair, and emitting nothing is a stroke the caller
881
+ * asked to be a picture and got in the theme colour without being told.
882
+ *
883
+ * Every stroke site resolves through here — the two `define/` rebuilds and the emitter — so
884
+ * the refusal names the call the caller made rather than surfacing at serialization time.
885
+ */
886
+ function resolveLineKind(props) {
887
+ const kind = resolveFillKind(props);
888
+ if (kind === "image") throw new UnsupportedFeatureError("line/image-fill-unsupported", "A picture stroke is not expressible in OOXML: `a:ln` accepts noFill/solidFill/gradFill/pattFill only. Use `fill: { image }` for a picture interior, or give the line a solid, gradient or pattern paint.");
889
+ return kind;
890
+ }
891
+ /**
892
+ * Does this fill or line props object name a paint at all?
893
+ *
894
+ * The question every *optional* paint site has to answer before dispatching: an outline, a
895
+ * slide background and a table cell each inherit when the caller said nothing, so emitting
896
+ * a fill child for an empty props object would paint them default black.
897
+ *
898
+ * "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.
900
+ * A `'solid'` kind needs a `color` to be saying anything; every other kind carries its
901
+ * payload in its own sub-object, so its presence is the statement.
902
+ *
903
+ * An empty `color` string answers false as well, but is reported on the way out
904
+ * ({@link rejectEmptyColor}): it is the caller's own missing value rather than a third
905
+ * spelling of silence, and it used to reach here from sites that painted it black.
906
+ */
907
+ function fillNamesPaint(props) {
908
+ if (!props) return false;
909
+ if (typeof props === "string") return !rejectEmptyColor(props, "color") && props.length > 0;
910
+ const kind = resolveFillKind(props);
911
+ if (kind === "inherit") return false;
912
+ if (kind === "solid") return !rejectEmptyColor(props.color, "color") && Boolean(props.color);
913
+ return true;
914
+ }
915
+ /**
916
+ * Reject the empty-colour spelling of a missing value, in either shape a fill can wear.
917
+ *
918
+ * `''` and `{ color: '' }` say the same thing — the caller had no colour — and only the
919
+ * second used to reach {@link createColorElement} and paint black. Both fold back onto
920
+ * *absence* here, which is not the same as "emit nothing": what an absent fill means is the
921
+ * surface's own rule, and a text box's absent fill is `<a:noFill/>`. So a site whose absence
922
+ * rule is anything other than silence calls this before it branches, rather than letting
923
+ * {@link genXmlColorSelection} answer for it.
924
+ * @param props - the caller's fill or line props, in any shape the option accepts
925
+ * @param option - the option's name, for the message
926
+ * @returns true when the props carried the empty colour string
927
+ */
928
+ function rejectEmptyFill(props, option) {
929
+ if (props === void 0 || typeof props === "string") return rejectEmptyColor(props, option);
930
+ return resolveFillKind(props) === "solid" && rejectEmptyColor(props.color, option);
931
+ }
932
+ /**
933
+ * A solid paint for {@link genXmlColorSelection}, carrying `transparency` only when one was set.
934
+ *
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
937
+ * of "no transparency": one that reads the same here, but not in a bag that is spread over
938
+ * another. So the key is simply absent, which is this codebase's one spelling of unset.
939
+ * @param color - the colour to paint, already resolved against whatever default applies
940
+ * @param transparency - percent transparency, or `undefined` for none
941
+ */
942
+ function solidPaint(color, transparency) {
943
+ return transparency === void 0 ? { color } : {
944
+ color,
945
+ transparency
946
+ };
947
+ }
948
+ /**
949
+ * Emit the fill group a set of fill/line props asks for, dispatching on
950
+ * {@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`.
952
+ * @param {Color | ShapeFillProps | ShapeLineProps | undefined} props fill props
953
+ * @returns XML string
954
+ */
955
+ function genXmlColorSelection(props) {
956
+ let colorVal = "";
957
+ let internalElements = "";
958
+ let outText = "";
959
+ if (rejectEmptyFill(props, "color")) return "";
960
+ if (props) {
961
+ const fillType = resolveFillKind(props);
962
+ if (typeof props === "string") colorVal = props;
963
+ else {
964
+ if (props.color) colorVal = props.color;
965
+ internalElements += alphaFromTransparency(props.transparency);
966
+ }
967
+ switch (fillType) {
968
+ case "none":
969
+ outText += voidEl("a:noFill");
970
+ break;
971
+ case "inherit":
972
+ outText += "";
973
+ break;
974
+ case "solid":
975
+ outText += el("a:solidFill", null, raw(createColorElement(colorVal, internalElements)));
976
+ break;
977
+ case "gradient":
978
+ outText += genXmlGradientFill(typeof props === "string" ? void 0 : props.gradient);
979
+ break;
980
+ case "pattern":
981
+ outText += genXmlPatternFill(typeof props === "string" ? void 0 : props.pattern);
982
+ break;
983
+ case "image":
984
+ outText += genXmlImageFill(typeof props === "string" ? void 0 : props);
985
+ break;
986
+ default: outText += "";
987
+ }
988
+ }
989
+ return outText;
990
+ }
991
+ //#endregion
992
+ //#region src/gen/drawingml/line.ts
993
+ /**
994
+ * Every key `BorderProps` defines.
995
+ *
996
+ * Derived from the interface rather than transcribed from it: the `satisfies` makes a key added
997
+ * to `BorderProps` and not to this list a compile error, where the old `readonly string[]` and a
998
+ * "keep in step" comment made it a silent omission.
999
+ */
1000
+ const BORDER_KEYS = Object.keys({
1001
+ type: 0,
1002
+ dashType: 0,
1003
+ color: 0,
1004
+ width: 0,
1005
+ transparency: 0,
1006
+ cap: 0
1007
+ });
1008
+ /**
1009
+ * Resolve an `a:prstDash/@val`, falling back when the caller named nothing or named something
1010
+ * outside `ST_PresetLineDashVal`.
1011
+ *
1012
+ * Nine sites in the emitters reach that attribute and five of them wrote the caller's value
1013
+ * straight through, so the SAME type on the SAME attribute got two answers: a table border's
1014
+ * `dashType: 'bogusDash'` warned and fell back to solid, a shape line's went into the part. A
1015
+ * value outside the union makes the part schema-invalid, which PowerPoint reports as a corrupt
1016
+ * file rather than as a mis-set option.
1017
+ * @param value - the caller's dash, if any
1018
+ * @param fallback - what to emit when the caller named none, or named one outside the union
1019
+ * @param label - how the diagnostic names the option, e.g. `'border: dashType'`
1020
+ * @returns a value legal for `a:prstDash/@val`
1021
+ */
1022
+ function resolveDash(value, fallback, label) {
1023
+ return checkEnumOrWarn(value, PRESET_LINE_DASHES, "border/invalid-dash-type", label) ?? fallback;
1024
+ }
1025
+ /**
1026
+ * Resolve the `a:prstDash/@val` a {@link StrokeProps} should emit.
1027
+ *
1028
+ * `type` is a three-way switch, so on its own it can only say "dashed" and every dashed
1029
+ * stroke collapses onto `sysDash`. `dashType` names the preset directly and therefore wins
1030
+ * when both are given; `type: 'none'` never reaches here, being decided by the caller before
1031
+ * any dash is chosen.
1032
+ * @param stroke - the caller's stroke properties
1033
+ * @param label - how the diagnostic names the option, e.g. `'border: dashType'`
1034
+ * @returns a value legal for `a:prstDash/@val`
1035
+ */
1036
+ function strokeDash(stroke, label) {
1037
+ return resolveDash(stroke.dashType, stroke.type === "dash" ? "sysDash" : "solid", label);
1038
+ }
1039
+ /**
1040
+ * {@link strokeDash} under the name the table and chart-area borders call it by.
1041
+ * @param {BorderProps} border - the caller's border properties
1042
+ * @returns {string} a value legal for `a:prstDash/@val`
1043
+ */
1044
+ function resolveBorderDash(border) {
1045
+ return strokeDash(border, "border: dashType");
1046
+ }
1047
+ /**
1048
+ * The `<a:solidFill>` (or `<a:noFill/>`) a {@link StrokeProps} paints with.
1049
+ *
1050
+ * `type: 'none'` is the one state that is not a colour, and it is decided here rather than at
1051
+ * each of the four chart call sites that used to spell it as a separate `*Show: false` flag or
1052
+ * as a `style: 'none'` sentinel.
1053
+ * @param stroke - the caller's stroke properties
1054
+ * @param defaultColor - the colour to paint when the caller named none
1055
+ * @returns the paint element
1056
+ */
1057
+ function strokePaint(stroke, defaultColor) {
1058
+ if (stroke.type === "none") return voidEl("a:noFill");
1059
+ return genXmlColorSelection(solidPaint(namedColorOr(stroke.color, defaultColor, "line.color"), stroke.transparency));
1060
+ }
1061
+ /**
1062
+ * Resolve an arrowhead type for `a:headEnd/@type` / `a:tailEnd/@type`, or `null` when the caller
1063
+ * named none -- the attribute is then not written and that end of the stroke stays plain.
1064
+ *
1065
+ * `ST_LineEndType` had no runtime tuple at all, so the two options were emitted verbatim and an
1066
+ * untyped caller could reach the attribute with anything.
1067
+ * @param value - the caller's arrow type, if any
1068
+ * @param label - how the diagnostic names the option, e.g. `'line: beginArrowType'`
1069
+ * @returns a value legal for the attribute, or `null` to omit it
1070
+ */
1071
+ function resolveLineEnd(value, label) {
1072
+ return checkEnumOrWarn(value, LINE_END_TYPES, "line/invalid-arrow-type", label);
1073
+ }
1074
+ /**
1075
+ * One arrowhead element (`<a:headEnd>` / `<a:tailEnd>`), or `''` when the caller named no arrow
1076
+ * or named one outside `ST_LineEndType`.
1077
+ * @param tag - `a:headEnd` or `a:tailEnd`
1078
+ * @param value - the caller's arrow type, if any
1079
+ * @param label - how the diagnostic names the option
1080
+ * @returns the element, or `''`
1081
+ */
1082
+ function lineEndEl(tag, value, label) {
1083
+ const type = resolveLineEnd(value, label);
1084
+ return type === null ? "" : voidEl(tag, { type });
1085
+ }
1086
+ /**
1087
+ * Report an authored key that is not part of `BorderProps`, which the library would
1088
+ * otherwise drop without a sound.
1089
+ *
1090
+ * TypeScript's excess-property check already rejects a stray key on a border written
1091
+ * *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
1093
+ * legitimately be a supertype. That exemption covers precisely the reuse pattern a
1094
+ * shared grid style encourages, so the mistake reaches here typed as valid and the
1095
+ * wrong-named value is discarded during generation. A `.pptx` gives no other signal:
1096
+ * nothing throws, the deck opens, and the border simply renders at its default weight.
1097
+ * @param border - the caller's border properties
1098
+ */
1099
+ function warnUnknownBorderKeys(border) {
1100
+ for (const key in border) {
1101
+ 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.`, {
1103
+ received: key,
1104
+ valid: BORDER_KEYS
1105
+ });
1106
+ }
1107
+ }
1108
+ /**
1109
+ * Resolve a border's line width in points, falling back to `defaultPt` when `width`
1110
+ * is not a number.
1111
+ *
1112
+ * A number that is not finite is not a missing width, so it goes on to the converter rather
1113
+ * than taking the default: `lineWidthToEmu` refuses `NaN` and clamps `Infinity` to the top of
1114
+ * `ST_LineWidth`, as it does for every other line width.
1115
+ *
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
1118
+ * place that can vet a border's shape for all of them ({@link warnUnknownBorderKeys}).
1119
+ * @param {BorderProps} border - border properties (may carry `width`)
1120
+ * @param {number} defaultPt - width to use when `width` is not a number
1121
+ * @returns {number} resolved width in points
1122
+ */
1123
+ function resolveBorderWidth(border, defaultPt) {
1124
+ warnUnknownBorderKeys(border);
1125
+ const val = border.width;
1126
+ return typeof val === "number" ? val : defaultPt;
1127
+ }
1128
+ /**
1129
+ * 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.
1131
+ *
1132
+ * Shapes, line-shaped text boxes and connectors each merged these themselves, and the connector
1133
+ * disagreed about zero: `line.width: 0` took the 1pt default on a shape and a text box, and wrote no
1134
+ * width at all on a connector. `0` means "not stated" for a line width, as it does for every option
1135
+ * `mapStated` guards, so all three take the default.
1136
+ *
1137
+ * The caller's line is spread first and only the defaulted keys are written over it. Listing the
1138
+ * carried keys instead is what once dropped `pattern`, `gradient` and `cap`: each was added to
1139
+ * `ShapeLineProps` without being added to the list, so the emitter read a key normalization had
1140
+ * already dropped. Only the solid arm writes `color`. The spread already carried whatever colour
1141
+ * another kind stated, and re-writing it would turn an unstated one into a present `undefined`.
1142
+ * @param line - the caller's line
1143
+ * @param colorLabel - how the caller's colour option is named in a colour warning
1144
+ * @returns the line with its defaults applied
1145
+ */
1146
+ function withLineDefaults(line, colorLabel) {
1147
+ const kind = resolveLineKind(line);
1148
+ const resolved = {
1149
+ ...line,
1150
+ type: kind,
1151
+ transparency: mapStated(line.transparency, (transparency) => transparency) ?? 0,
1152
+ width: mapStated(line.width, (width) => width) ?? 1,
1153
+ dashType: line.dashType || "solid"
1154
+ };
1155
+ if (kind === "solid") resolved.color = namedColorOr(line.color, DEF_SHAPE_LINE_COLOR, colorLabel);
1156
+ return resolved;
1157
+ }
1158
+ /**
1159
+ * Map a friendly `LineCap` value to the OOXML `cap` attribute value (`flat`/`sq`/`rnd`).
1160
+ * @param {LineCap} [lineCap] - line cap style (defaults to `flat`)
1161
+ * @returns {string} value for the `cap` attribute on `<a:ln>`
1162
+ */
1163
+ function createLineCap(lineCap) {
1164
+ if (!lineCap) return "flat";
1165
+ const token = lineCapToken(lineCap);
1166
+ if (token === void 0) throw new InvalidOptionError("line/invalid-cap", `Invalid line cap: ${String(lineCap)}`);
1167
+ return token;
1168
+ }
1169
+ /**
1170
+ * Emit the paint child of an `<a:ln>` stroke.
1171
+ *
1172
+ * `<a:ln>` takes almost the same fill group as a shape interior, so a stroke can be a gradient
1173
+ * 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
1175
+ * sub-object while the interior inferred nothing, so the same `{ gradient }` spelling painted a
1176
+ * gradient outline and a black fill. "Almost" is the picture fill: `EG_LineFillProperties` has
1177
+ * no `a:blipFill`, and `resolveLineKind` is where that is refused.
1178
+ *
1179
+ * Returns '' when the line names no paint at all ({@link fillNamesPaint}), so the caller emits
1180
+ * no fill child and the stroke inherits its color from the theme or placeholder. `type: 'none'`
1181
+ * is not that case: it is an explicit *no stroke*, and a shape authored with it grew the theme's
1182
+ * border back when the two were conflated.
1183
+ * @param {ShapeLineProps} line line options
1184
+ * @returns XML string
1185
+ */
1186
+ function genXmlLineFill(line) {
1187
+ if (!fillNamesPaint(line)) return "";
1188
+ resolveLineKind(line);
1189
+ return genXmlColorSelection(line);
1190
+ }
1191
+ /**
1192
+ * The `<a:ln>`-shaped stroke a {@link BorderProps} paints, in the one place every border
1193
+ * emitter now derives it.
1194
+ *
1195
+ * Four builders used to turn a `BorderProps` into a line element, each with its own
1196
+ * defaults, and the differences between them were only visible by reading all four: a
1197
+ * 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
1201
+ * those slots. Those are this function's parameters, so each caller is now a named
1202
+ * argument set rather than a re-derivation.
1203
+ *
1204
+ * Two things are deliberately *not* parameters. A stroke that paints nothing is spelled
1205
+ * differently by each caller (a bare `<a:ln><a:noFill/></a:ln>` for a chart, an
1206
+ * 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
1208
+ * spelling of the first. And the colour default is taken on `||`, not `??`, so an empty
1209
+ * `color` reads as *unstated* rather than reaching the colour validator as a bad value:
1210
+ * 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
1212
+ * default they had named a line earlier. The chart-border builder already read it this way,
1213
+ * and `dataBorder` is the only one of the other three a caller can actually reach with an
1214
+ * empty string; the rest resolve their colour in the definition step first.
1215
+ *
1216
+ * @param name - the element's qualified name (`a:ln`, or `a:lnL`/`a:lnTlToBr`/… for a table cell)
1217
+ * @param border - the caller's border properties
1218
+ * @param spec - `defaultWidth`/`defaultColor` are used when the border states neither;
1219
+ * `cap` is the `cap` attribute value, already resolved; `extraAttrs` follows it in
1220
+ * document order; `dash` is the `a:prstDash/@val` to write, or `null` to omit the
1221
+ * element; `tail` is the already-built children that follow the dash (`a:round`, and a
1222
+ * table cell's head/tail ends), in schema order.
1223
+ * @returns the border element XML
1224
+ */
1225
+ function borderLine(name, border, spec) {
1226
+ return el(name, {
1227
+ w: lineWidthToEmu(resolveBorderWidth(border, spec.defaultWidth), "border.width"),
1228
+ cap: spec.cap,
1229
+ ...spec.extraAttrs
1230
+ }, [
1231
+ genXmlColorSelection(solidPaint(namedColorOr(border.color, spec.defaultColor, "border.color"), border.transparency)),
1232
+ ...spec.dash === null ? [] : [voidEl("a:prstDash", { val: spec.dash })],
1233
+ ...spec.tail
1234
+ ].map(raw));
1235
+ }
1236
+ /**
1237
+ * The `<a:ln>` that paints no stroke at all: an explicit `<a:noFill/>` on a bare element.
1238
+ *
1239
+ * 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
1241
+ * `a:lnRef`, where this overrides it. A table cell spells the same intent with the
1242
+ * attributes still on the element and `w="0"`, which is why that one is not this.
1243
+ * @returns the `<a:ln><a:noFill/></a:ln>` element XML
1244
+ */
1245
+ function noStrokeLine() {
1246
+ return el("a:ln", null, raw(voidEl("a:noFill")));
1247
+ }
1248
+ //#endregion
1249
+ //#region src/gen/define/hyperlinks.ts
1250
+ /**
1251
+ * The relationship record one hyperlink serializes to.
1252
+ *
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
1255
+ * every emitter escapes it on the way out (see the note on `SlideRel.Target`).
1256
+ *
1257
+ * Four sites built this record by hand: the two below, `addImage`'s own hyperlink, and the
1258
+ * notes-part rels, which allocate from their own reserved id space and so cannot share the
1259
+ * registration below but can share this.
1260
+ * @param rId - the relationship id already allocated for this hyperlink
1261
+ * @param hyperlink - the caller's hyperlink; only `url`/`slide` are read
1262
+ */
1263
+ function hyperlinkRel(rId, hyperlink) {
1264
+ return {
1265
+ type: "hyperlink",
1266
+ data: hyperlink.url ? "dummy" : "slide",
1267
+ rId,
1268
+ Target: hyperlink.url ? hyperlink.url : String(hyperlink.slide)
1269
+ };
1270
+ }
1271
+ /**
1272
+ * Refuse a hyperlink the writer cannot express, the same way wherever one is authored.
1273
+ *
1274
+ * A hyperlink names exactly one destination: a `url`, a `slide`, or a slide-show `action`. The image
1275
+ * definer and the run emitter each checked their own subset, and the shape path checked nothing. An
1276
+ * image refused an action-only link that a shape accepted, and nothing refused `url` together with
1277
+ * `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.
1279
+ *
1280
+ * A `slide` has to be a 1-based slide number. `-1` and `1.5` were written as the targets
1281
+ * `slide-1.xml` and `slide1.5.xml`, parts no deck has. Whether the numbered slide exists is only
1282
+ * known once the deck is complete, so a number past the last slide is refused when the deck is
1283
+ * written (`requireSlideLinksInDeck`).
1284
+ * @param hyperlink - the caller's `hyperlink` option
1285
+ * @param call - the method or option the link was authored through, opening the message
1286
+ */
1287
+ function validateHyperlink(hyperlink, call) {
1288
+ 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
+ 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\`.`);
1291
+ 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
+ if (!url && !slide && !action) throw new InvalidOptionError("hyperlink/missing-target", `${call}: \`hyperlink\` requires either \`url\`, \`slide\`, or \`action\``);
1293
+ }
1294
+ /**
1295
+ * Mint a fresh rel id for `hyperlink`, register it on `target`, and stamp the id back onto
1296
+ * the hyperlink so the emitter can write `r:id`.
1297
+ *
1298
+ * 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
1301
+ * hyperlink and emitting a duplicate `Relationship Id`.
1302
+ * @returns the allocated relationship id
1303
+ */
1304
+ function registerHyperlinkRel(target, hyperlink) {
1305
+ const relId = getNewRelId(target);
1306
+ target._rels.push(hyperlinkRel(relId, hyperlink));
1307
+ hyperlink._rId = relId;
1308
+ return relId;
1309
+ }
1310
+ /** The objects one level of a text or table-cell tree holds: a table row is itself an array. */
1311
+ 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] : [];
1315
+ }
1316
+ /**
1317
+ * Parses text/text-objects from `addText()` and `addTable()` methods; creates 'hyperlink'-type Slide Rels for each hyperlink found
1318
+ *
1319
+ * Every link in the tree is checked before any is registered, by the same rules as an image's. A
1320
+ * refused link on a second run used to throw after the first run's relationship was already on
1321
+ * the slide, left pointing at a shape that was never added.
1322
+ * @param {PresSlideInternal} target - slide object that any hyperlinks will be be added to
1323
+ * @param {number | string | TextProps | TextProps[] | TableCellInternal[][]} text - text to parse
1324
+ * @param call - the method the objects were authored through, for a refused hyperlink's message
1325
+ */
1326
+ function createHyperlinkRels(target, text, call = "addText") {
1327
+ validateHyperlinkTree(text, call);
1328
+ registerHyperlinkTree(target, text);
1329
+ }
1330
+ /** Refuse the first hyperlink in the tree the writer cannot express, before anything is registered. */
1331
+ function validateHyperlinkTree(text, call) {
1332
+ for (const item of hyperlinkTreeLevel(text)) {
1333
+ if (Array.isArray(item)) {
1334
+ validateHyperlinkTree(item, call);
1335
+ continue;
1336
+ }
1337
+ if (item.options?.hyperlink) validateHyperlink(item.options.hyperlink, call);
1338
+ if (Array.isArray(item.text)) validateHyperlinkTree(item.text, call);
1339
+ }
1340
+ }
1341
+ /** Register a relationship for every hyperlink in an already-validated tree. */
1342
+ function registerHyperlinkTree(target, tree) {
1343
+ hyperlinkTreeLevel(tree).forEach((text) => {
1344
+ if (Array.isArray(text)) {
1345
+ registerHyperlinkTree(target, text);
1346
+ return;
1347
+ }
1348
+ if (Array.isArray(text.text)) registerHyperlinkTree(target, text.text);
1349
+ const options = text.options;
1350
+ const hyperlink = options?.hyperlink;
1351
+ if (!options || !hyperlink) return;
1352
+ if (!hyperlink._rId) {
1353
+ if (hyperlink.url || hyperlink.slide) registerHyperlinkRel(target, hyperlink);
1354
+ return;
1355
+ }
1356
+ const hyperlinkRelId = hyperlink._rId;
1357
+ const wanted = hyperlinkRel(hyperlinkRelId, hyperlink);
1358
+ if (target._rels.some((rel) => rel.rId === hyperlinkRelId && rel.type === wanted.type && rel.data === wanted.data && rel.Target === wanted.Target)) return;
1359
+ if (!heldRelIds(target).has(hyperlinkRelId)) target._rels.push(wanted);
1360
+ else {
1361
+ const own = { ...hyperlink };
1362
+ delete own._rId;
1363
+ options.hyperlink = own;
1364
+ registerHyperlinkRel(target, own);
1365
+ }
1366
+ });
1367
+ }
1368
+ //#endregion
1369
+ //#region src/gen/drawingml/bullet.ts
1370
+ /** A bullet `characterCode`: four hex digits naming a code point. */
1371
+ const CHARACTER_CODE = /^[0-9A-Fa-f]{4}$/;
1372
+ /** `ST_TextBulletStartAtNum`'s range. */
1373
+ const START_AT_MIN = 1;
1374
+ const START_AT_MAX = 32767;
1375
+ /**
1376
+ * `<a:buChar>`: the glyph a caller's `characterCode` names, or `glyph` when the caller named none.
1377
+ *
1378
+ * A template rather than `voidEl`, because `char` carries a pre-escaped numeric character reference
1379
+ * (`&#x2022;`, see `BulletType`) and the builder would escape its `&` a second time, rendering the
1380
+ * reference as literal text. That is why the code is checked before it gets near the attribute: one
1381
+ * that is not four hex digits warns and becomes {@link BulletType.DEFAULT}.
1382
+ * @param characterCode - the caller's `characterCode`, if they gave one
1383
+ * @param glyph - the glyph when they gave none; a library constant, already safe in an attribute
1384
+ * @param label - the option the code came from, opening the warning
1385
+ */
1386
+ function buCharEl(characterCode, glyph, label) {
1387
+ let char = glyph;
1388
+ if (characterCode) {
1389
+ if (CHARACTER_CODE.test(characterCode)) char = `&#x${characterCode};`;
1390
+ else {
1391
+ warn("bullet/invalid-character-code", `${label} \`characterCode\` should be a 4-digit unicode character (ex: 22AB), got ${JSON.stringify(characterCode)}; using the default bullet`);
1392
+ char = "&#x2022;";
1393
+ }
1394
+ }
1395
+ return `<a:buChar char="${char}"/>`;
1396
+ }
1397
+ /**
1398
+ * `<a:buAutoNum>` for an auto-number scheme, starting at `startAt` when one is stated.
1399
+ *
1400
+ * `type` is required on the element, so a scheme `ST_TextAutonumberScheme` does not have warns and
1401
+ * the list counts in `arabicPeriod`, the scheme a numbered bullet takes when none is stated.
1402
+ * `startAt` is an `ST_TextBulletStartAtNum`, a whole number from 1 to 32767: a fractional one rounds,
1403
+ * one outside the range clamps with a warning, and one that is not a number throws.
1404
+ * @param type - the `ST_TextAutonumberScheme` token
1405
+ * @param startAt - the number the list starts at; omitted from the element when `undefined`
1406
+ * @param label - the option the value came from, opening the warning
1407
+ */
1408
+ function buAutoNumEl(type, startAt, label) {
1409
+ return voidEl("a:buAutoNum", {
1410
+ type: checkEnumOrWarn(type, TEXT_AUTONUM_SCHEMES, "bullet/invalid-number-type", `${label} \`numberType\``) ?? "arabicPeriod",
1411
+ 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
+ }
1414
+ //#endregion
1415
+ //#region src/gen/drawingml/clamp.ts
1416
+ /**
1417
+ * ts-pptx: DrawingML value clamps
1418
+ *
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
1421
+ * percent for the one measure the caller states as a multiple rather than a size.
1422
+ * Out-of-range values make PowerPoint report the package as needing repair.
1423
+ */
1424
+ /**
1425
+ * Clamp a caller's value into its schema range, warn once if it did not already fit, and convert
1426
+ * it to the attribute's unit.
1427
+ *
1428
+ * The range is applied in the caller's own unit, through {@link clampRangedInput}: `Infinity`
1429
+ * clamps to the bound and warns, and `NaN` throws naming the option. Converting first left a
1430
+ * non-finite value to whatever the converter did with it, so `fontSize: Infinity` threw without
1431
+ * naming the option while `transparency: Infinity` clamped and warned. A value within half a unit
1432
+ * of a bound that used to round into range now also warns, though it writes the same bytes.
1433
+ *
1434
+ * The measures below differ only in the fields of {@link ClampSpec}. The warning is deduped on its
1435
+ * code plus its message, so the wording is part of the contract, not decoration.
1436
+ */
1437
+ function clampWithWarn(value, spec, label = spec.label) {
1438
+ const clamped = clampRangedInput(value, spec.min / spec.perUnit, spec.max / spec.perUnit, spec.code, label, spec.nonFiniteCode, {
1439
+ once: true,
1440
+ range: spec.range
1441
+ });
1442
+ return spec.convert(clamped);
1443
+ }
1444
+ const FONT_SIZE = {
1445
+ code: "font/size-out-of-range",
1446
+ nonFiniteCode: "coord/non-finite",
1447
+ label: "fontSize",
1448
+ range: `1-${MAX_FONT_SIZE_PT}pt`,
1449
+ convert: ptToHundredths,
1450
+ perUnit: 100,
1451
+ min: 100,
1452
+ max: MAX_FONT_SIZE_PT * 100
1453
+ };
1454
+ const CHAR_SPACING = {
1455
+ code: "text/char-spacing-out-of-range",
1456
+ nonFiniteCode: "coord/non-finite",
1457
+ label: "charSpacing",
1458
+ range: "-4000..4000pt",
1459
+ convert: ptToHundredths,
1460
+ perUnit: 100,
1461
+ min: -4e5,
1462
+ max: 4e5
1463
+ };
1464
+ const PARA_MARGIN = {
1465
+ code: "text/paragraph-margin-out-of-range",
1466
+ nonFiniteCode: "coord/non-finite",
1467
+ label: "paraMarginLeft",
1468
+ range: "0-4032pt",
1469
+ convert: ptsToEmuLenient,
1470
+ perUnit: EMU_PER_POINT,
1471
+ min: 0,
1472
+ max: 51206400
1473
+ };
1474
+ const PARA_INDENT = {
1475
+ code: "text/paragraph-indent-out-of-range",
1476
+ nonFiniteCode: "coord/non-finite",
1477
+ label: "paraIndent",
1478
+ range: "-4032..4032pt",
1479
+ convert: ptsToEmuLenient,
1480
+ perUnit: EMU_PER_POINT,
1481
+ min: -51206400,
1482
+ max: 51206400
1483
+ };
1484
+ /** {@link PARA_MARGIN}, for the callers that state the measure in inches. 4032pt is 56in. */
1485
+ const PARA_MARGIN_INCHES = {
1486
+ ...PARA_MARGIN,
1487
+ range: "0-56in",
1488
+ convert: inch2Emu,
1489
+ perUnit: EMU_PER_INCH
1490
+ };
1491
+ /** {@link PARA_INDENT}, for the callers that state the measure in inches. */
1492
+ const PARA_INDENT_INCHES = {
1493
+ ...PARA_INDENT,
1494
+ range: "-56..56in",
1495
+ convert: inch2Emu,
1496
+ perUnit: EMU_PER_INCH
1497
+ };
1498
+ const LINE_SPACING = {
1499
+ code: "text/line-spacing-out-of-range",
1500
+ nonFiniteCode: "coord/non-finite",
1501
+ label: "lineSpacing",
1502
+ range: "0-1584pt",
1503
+ convert: ptToHundredths,
1504
+ perUnit: 100,
1505
+ min: 0,
1506
+ max: 158400
1507
+ };
1508
+ const LINE_SPACING_MULTIPLE = {
1509
+ code: "text/line-spacing-out-of-range",
1510
+ nonFiniteCode: "percent/non-finite",
1511
+ label: "lineSpacingMultiple",
1512
+ range: "0-132",
1513
+ convert: (multiple) => Math.round(multiple * PERCENT_SCALE),
1514
+ perUnit: PERCENT_SCALE,
1515
+ min: 0,
1516
+ max: 132e5
1517
+ };
1518
+ /**
1519
+ * Clamp a font size (points) into ST_TextFontSize (1-4000pt) and return it in
1520
+ * hundredths of a point for the `sz` attribute. Out-of-range sizes make
1521
+ * PowerPoint report the package as needing repair (e.g. `sz` > 400000 or < 100).
1522
+ *
1523
+ * Every `sz` in the package goes through here, and the nine options that reach it are spelled
1524
+ * nine different ways -- `fontSize` on a run, `legendFontSize` and six siblings on a chart.
1525
+ * `label` is what the warning quotes back, so a caller who set `catAxisLabelFontSize` is not
1526
+ * told about `fontSize`.
1527
+ * @param fontSizePts - the caller's size in points
1528
+ * @param label - option name as the caller spells it
1529
+ */
1530
+ function clampFontSizeSz(fontSizePts, label) {
1531
+ return clampWithWarn(fontSizePts, FONT_SIZE, label);
1532
+ }
1533
+ /** Clamp character spacing (points) into ST_TextPoint (-4000..4000pt); returns hundredths for the `spc` attribute. */
1534
+ function clampCharSpacingSpc(charSpacingPts) {
1535
+ return clampWithWarn(charSpacingPts, CHAR_SPACING);
1536
+ }
1537
+ /**
1538
+ * 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,
1540
+ * it is a value PowerPoint reports as needing repair.
1541
+ */
1542
+ function clampParaMarginEmu(marginPts) {
1543
+ return clampWithWarn(marginPts, PARA_MARGIN);
1544
+ }
1545
+ /**
1546
+ * Clamp a first-line indent (points) into ST_TextIndent (-4032..4032pt); returns EMU for
1547
+ * `a:pPr/@indent`. Unlike the margin this one is signed: a negative indent is the hanging
1548
+ * indent every bulleted paragraph uses.
1549
+ */
1550
+ function clampParaIndentEmu(indentPts) {
1551
+ return clampWithWarn(indentPts, PARA_INDENT);
1552
+ }
1553
+ /**
1554
+ * Clamp a left margin the caller stated in INCHES into ST_TextMargin, returning EMU for
1555
+ * `a:pPr/@marL`.
1556
+ *
1557
+ * A master's text styles state their measures in inches where a run's paragraph properties
1558
+ * state them in points. The schema bound is the same one either way, so the two spellings
1559
+ * share {@link PARA_MARGIN}'s range and differ only in the unit the warning quotes back.
1560
+ * @param marginInches - the caller's margin in inches
1561
+ * @param label - option name as the caller spells it
1562
+ */
1563
+ function clampParaMarginInchesEmu(marginInches, label) {
1564
+ return clampWithWarn(marginInches, PARA_MARGIN_INCHES, label);
1565
+ }
1566
+ /**
1567
+ * Clamp a first-line indent the caller stated in INCHES into ST_TextIndent, returning EMU for
1568
+ * `a:pPr/@indent`. The inch-stated half of {@link clampParaIndentEmu}; see
1569
+ * {@link clampParaMarginInchesEmu} for why there are two.
1570
+ * @param indentInches - the caller's indent in inches
1571
+ * @param label - option name as the caller spells it
1572
+ */
1573
+ function clampParaIndentInchesEmu(indentInches, label) {
1574
+ return clampWithWarn(indentInches, PARA_INDENT_INCHES, label);
1575
+ }
1576
+ /** Clamp line spacing (points) into ST_TextSpacingPoint (0..1584pt); returns hundredths for `<a:spcPts val>`. */
1577
+ function clampLineSpacingPts(lineSpacingPts) {
1578
+ return clampWithWarn(lineSpacingPts, LINE_SPACING);
1579
+ }
1580
+ /**
1581
+ * Clamp a line-spacing multiple into ST_TextSpacingPercentOrPercentString (0..13200000, i.e. a
1582
+ * multiple of 0..132); returns thousandths of a percent for `<a:spcPct val>`. The bound is the
1583
+ * schema's, not the `0.0-9.99` the option documents: a multiple of 12 is unusual, not invalid,
1584
+ * and only the values PowerPoint reports as needing repair are worth moving.
1585
+ */
1586
+ function clampLineSpacingMultiplePct(multiple) {
1587
+ return clampWithWarn(multiple, LINE_SPACING_MULTIPLE);
1588
+ }
1589
+ //#endregion
1590
+ //#region src/gen/drawingml/math.ts
1591
+ /**
1592
+ * ts-pptx: DrawingML native equations (OMML)
1593
+ *
1594
+ * Wrap raw OMML markup into the `<a14:m>` marker PowerPoint uses for editable
1595
+ * equations — as a standalone display paragraph (`genXmlMathParagraph`) or as an
1596
+ * inline run that flows between the surrounding `<a:r>` runs (`genXmlInlineMath`).
1597
+ */
1598
+ /** The `<a14:m>` marker, carrying both namespace declarations so the supplied OMML needs none. */
1599
+ function a14Math(mathXml) {
1600
+ return el("a14:m", {
1601
+ "xmlns:a14": OOXML_NS.a14,
1602
+ "xmlns:m": OOXML_NS.m
1603
+ }, raw(mathXml));
1604
+ }
1605
+ /**
1606
+ * Build a native-equation paragraph (`<a:p>`) from raw OMML.
1607
+ *
1608
+ * PowerPoint stores an editable equation inside a text body as an `<a14:m>` marker wrapping
1609
+ * `<m:oMathPara><m:oMath>…`. We declare both the `a14` (drawing-2010) and `m` (math) namespaces
1610
+ * on the `<a14:m>` element so the supplied OMML needs no namespace declarations of its own, then
1611
+ * accept three input shapes: a full `<m:oMathPara>`, a full `<m:oMath>`, or the inner OMML
1612
+ * (children of `<m:oMath>`). A trailing `<a:endParaRPr>` matches what PowerPoint authors.
1613
+ *
1614
+ * @param {string} omml - raw OMML markup for the equation
1615
+ * @returns {string} an `<a:p>` math paragraph
1616
+ */
1617
+ function genXmlMathParagraph(omml) {
1618
+ const trimmed = (omml || "").trim();
1619
+ const paraPr = el("m:oMathParaPr", null, raw(voidEl("m:jc", { "m:val": "centerGroup" })));
1620
+ 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
+ }
1622
+ /**
1623
+ * Build an INLINE native-equation run (dn-inline-math) from raw OMML.
1624
+ *
1625
+ * Unlike the display form ({@link genXmlMathParagraph}), an inline equation is *not* its own
1626
+ * 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>` —
1628
+ * pinned by the `math-omml-inline.pptx` oracle. The `mc:AlternateContent` envelope stays at the
1629
+ * shape level (see `objectHasMath`), so nothing wraps the run itself. We declare the `a14` and `m`
1630
+ * namespaces on the `<a14:m>` element so the supplied OMML needs none of its own, and accept the
1631
+ * same three input shapes as the display helper: a full `<m:oMathPara>` (its inner `<m:oMath>` is
1632
+ * unwrapped, since a paragraph block cannot flow inline), a full `<m:oMath>`, or the inner OMML.
1633
+ *
1634
+ * @param {string} omml - raw OMML markup for the equation
1635
+ * @returns {string} an `<a14:m>` inline equation run
1636
+ */
1637
+ function genXmlInlineMath(omml) {
1638
+ const trimmed = (omml || "").trim();
1639
+ const oMathMatch = trimmed.match(/<m:oMath[\s>][\s\S]*<\/m:oMath>/);
1640
+ return a14Math(oMathMatch ? oMathMatch[0] : el("m:oMath", null, raw(trimmed)));
1641
+ }
1642
+ //#endregion
1643
+ //#region src/gen/drawingml/text-run.ts
1644
+ /**
1645
+ * `<a:extLst>` marking a hyperlink whose color should follow the text color rather than the
1646
+ * theme's hyperlink color. The leading spaces are byte-significant: this block was authored as
1647
+ * indented string concatenation and that indentation reaches the file, so it is described here
1648
+ * with `openPrefix`/`closePrefix` rather than being tidied away.
1649
+ */
1650
+ 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",
1652
+ val: "tx"
1653
+ }, { openPrefix: " " })), {
1654
+ openPrefix: " ",
1655
+ closePrefix: " "
1656
+ })), {
1657
+ openPrefix: " ",
1658
+ closePrefix: " "
1659
+ });
1660
+ /**
1661
+ * Whether a `bullet` value makes the paragraph DRAW a bullet (a glyph, a number or a picture).
1662
+ *
1663
+ * Two call sites downstream of the emitter act on "this paragraph has a bullet" rather than on
1664
+ * "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
1666
+ * would have broken the line grouping (every run starting its own paragraph) and eaten a real
1667
+ * leading `•` from text that emits no `a:buChar` to duplicate it.
1668
+ * @param {TextPropsOptions['bullet']} bullet - the option value
1669
+ * @return {boolean} true when bullet markup is emitted
1670
+ */
1671
+ function emitsBulletMarkup(bullet) {
1672
+ return !!bullet && bullet !== "inherit";
1673
+ }
1674
+ /**
1675
+ * One paragraph margin attribute's value in EMU, or `null` when the attribute is not written.
1676
+ *
1677
+ * Three inputs, because the option has three states and so does the attribute: an explicit
1678
+ * 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
1680
+ * that states nothing on its own.
1681
+ *
1682
+ * BOTH arms go through the clamp. The fallback used to be returned verbatim on the grounds that
1683
+ * 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
1685
+ * ~6700x the `ST_TextMargin` ceiling. The fallback arrives in EMU rather than points because the
1686
+ * bullet arms do their arithmetic there; the round trip through the clamp's own unit is exact
1687
+ * for any value in range, so a paragraph that was already legal emits the same bytes.
1688
+ * @param {number|'inherit'|undefined} option - `paraMarginLeft` / `paraIndent` as given
1689
+ * @param {number|null} fallbackEmu - the bullet arm's default, `null` for no attribute
1690
+ * @param {Function} clamp - the attribute's ST_* range clamp (points in, EMU out)
1691
+ * @return {number|null} the EMU value to write, or null to omit the attribute
1692
+ */
1693
+ function resolveParagraphMargin(option, fallbackEmu, clamp) {
1694
+ if (option === "inherit") return null;
1695
+ if (option !== void 0) return clamp(option);
1696
+ return fallbackEmu === null ? null : clamp(fallbackEmu / EMU_PER_POINT);
1697
+ }
1698
+ /**
1699
+ * The paragraph's outline level, or `0` when the caller named none or named one `a:p/@lvl`
1700
+ * cannot hold.
1701
+ *
1702
+ * Resolved once because the level is read twice — for the attribute, and as the multiplier on
1703
+ * the bullet arms' default `@marL`. Validating it for one use and multiplying a margin by it in
1704
+ * the other is how `indentLevel: 1e6` reached `marL="342900342900"` under a warning that said
1705
+ * the value was being ignored.
1706
+ * @param {number|undefined} level - `indentLevel` as given
1707
+ * @return {number} a level `ST_TextIndentLevelType` accepts
1708
+ */
1709
+ function resolveIndentLevel(level) {
1710
+ if (!level) return 0;
1711
+ if (Number.isInteger(level) && level > 0 && level <= 8) return level;
1712
+ warn("text/invalid-indent-level", `indentLevel ${String(level)} must be a whole number from 0 to 8; ignoring it.`);
1713
+ return 0;
1714
+ }
1715
+ /**
1716
+ * 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
1719
+ * and the other left behind.
1720
+ * @param {number} bulletMarLEmu - the hang one level is worth, in EMU
1721
+ * @param {number} indentLevel - the resolved outline level
1722
+ * @return {number} `a:pPr/@marL` in EMU
1723
+ */
1724
+ function bulletHangMarL(bulletMarLEmu, indentLevel) {
1725
+ return bulletMarLEmu + bulletMarLEmu * indentLevel;
1726
+ }
1727
+ /**
1728
+ * Generate XML Paragraph Properties
1729
+ * @param {SlideObject|TextProps} textObj - text object
1730
+ * @param {boolean} isDefault - array of default relations
1731
+ * @return {string} XML
1732
+ */
1733
+ function genXmlParagraphProperties(textObj, isDefault) {
1734
+ const opts = textObj.options ?? {};
1735
+ let strXmlBullet = "";
1736
+ let strXmlBulletColor = "";
1737
+ let strXmlLnSpc = "";
1738
+ let strXmlParaSpc = "";
1739
+ let strXmlTabStops = "";
1740
+ const tag = isDefault ? "a:lvl1pPr" : "a:pPr";
1741
+ let bulletMarL = ptsToEmuLenient(27);
1742
+ let defaultMarL = null;
1743
+ let defaultIndent = null;
1744
+ let indentBeforeMarL = false;
1745
+ let paragraphPropXml = `<${tag}${opts.rtlMode ? " rtl=\"1\" " : ""}`;
1746
+ {
1747
+ const algn = opts.align ? textAlignToken(opts.align) : void 0;
1748
+ if (algn) paragraphPropXml += ` algn="${algn}"`;
1749
+ const spacingPts = mapStated(opts.lineSpacing, clampLineSpacingPts);
1750
+ const spacingPct = spacingPts === void 0 ? mapStated(opts.lineSpacingMultiple, clampLineSpacingMultiplePct) : void 0;
1751
+ if (spacingPts !== void 0) strXmlLnSpc = el("a:lnSpc", null, raw(voidEl("a:spcPts", { val: spacingPts })));
1752
+ else if (spacingPct !== void 0) strXmlLnSpc = el("a:lnSpc", null, raw(voidEl("a:spcPct", { val: spacingPct })));
1753
+ const indentLevel = resolveIndentLevel(opts.indentLevel);
1754
+ if (indentLevel) paragraphPropXml += ` lvl="${indentLevel}"`;
1755
+ if (emitsParagraphSpacing(opts.paraSpaceBefore)) strXmlParaSpc += el("a:spcBef", null, raw(voidEl("a:spcPts", { val: ptToHundredths(opts.paraSpaceBefore) })));
1756
+ if (emitsParagraphSpacing(opts.paraSpaceAfter)) strXmlParaSpc += el("a:spcAft", null, raw(voidEl("a:spcPts", { val: ptToHundredths(opts.paraSpaceAfter) })));
1757
+ if (typeof opts.bullet === "object") {
1758
+ const bullet = opts.bullet;
1759
+ const bulletImage = bullet.image;
1760
+ const isPictureBullet = !!(bulletImage && (bulletImage.path || bulletImage.data));
1761
+ if (opts.bullet?.indent) bulletMarL = ptsToEmuLenient(opts.bullet.indent);
1762
+ defaultMarL = bulletHangMarL(bulletMarL, indentLevel);
1763
+ defaultIndent = -bulletMarL;
1764
+ rejectEmptyColor(opts.bullet.color, "bullet.color");
1765
+ if (opts.bullet.color && !isPictureBullet) strXmlBulletColor = el("a:buClr", null, raw(createColorElement(opts.bullet.color)));
1766
+ const bulletSizePct = opts.bullet.size === void 0 ? void 0 : percentToFixedPercent(opts.bullet.size, "bullet/size-out-of-range", "bullet.size", 25, 400);
1767
+ const strXmlBulletSize = bulletSizePct === void 0 ? "" : voidEl("a:buSzPct", { val: bulletSizePct });
1768
+ const strXmlBulletFont = opts.bullet.fontFace ? voidEl("a:buFont", { typeface: opts.bullet.fontFace }) : "";
1769
+ if (isPictureBullet) {
1770
+ if (bullet._rId) {
1771
+ const svgExt = bullet._rIdSvg ? el("a:extLst", null, raw(el("a:ext", { uri: "{96DAC541-7B7A-43D3-8B79-37D633B846F1}" }, raw(voidEl("asvg:svgBlip", {
1772
+ "xmlns:asvg": OOXML_NS.asvg,
1773
+ "r:embed": `rId${bullet._rIdSvg}`
1774
+ }))))) : "";
1775
+ strXmlBullet = strXmlBulletSize + el("a:buBlip", null, raw(elOrVoid("a:blip", { "r:embed": `rId${bullet._rId}` }, svgExt)));
1776
+ } else {
1777
+ warn("bullet/image-embed-failed", "picture `bullet.image` could not be embedded; using a default bullet glyph");
1778
+ strXmlBullet = strXmlBulletSize + strXmlBulletFont + buCharEl(void 0, "&#x2022;", "bullet");
1779
+ }
1780
+ } else if (opts.bullet.type && opts.bullet.type.toString().toLowerCase() === "number") strXmlBullet = strXmlBulletSize + (strXmlBulletFont || voidEl("a:buFont", { typeface: "+mj-lt" })) + buAutoNumEl(opts.bullet.numberType || "arabicPeriod", opts.bullet.numberStartAt || 1, "bullet");
1781
+ else strXmlBullet = strXmlBulletSize + strXmlBulletFont + buCharEl(opts.bullet.characterCode, "&#x2022;", "bullet");
1782
+ } else if (opts.bullet === "inherit") {} else if (opts.bullet) {
1783
+ defaultMarL = bulletHangMarL(bulletMarL, indentLevel);
1784
+ defaultIndent = -bulletMarL;
1785
+ strXmlBullet = buCharEl(void 0, "&#x2022;", "bullet");
1786
+ } else if (!opts.bullet) {
1787
+ defaultMarL = 0;
1788
+ defaultIndent = 0;
1789
+ indentBeforeMarL = true;
1790
+ strXmlBullet = voidEl("a:buNone");
1791
+ }
1792
+ const marLEmu = resolveParagraphMargin(opts.paraMarginLeft, defaultMarL, clampParaMarginEmu);
1793
+ const indentEmu = resolveParagraphMargin(opts.paraIndent, defaultIndent, clampParaIndentEmu);
1794
+ const marLAttr = marLEmu === null ? "" : ` marL="${marLEmu}"`;
1795
+ const indentAttr = indentEmu === null ? "" : ` indent="${indentEmu}"`;
1796
+ paragraphPropXml += indentBeforeMarL ? indentAttr + marLAttr : marLAttr + indentAttr;
1797
+ if (opts.tabStops && Array.isArray(opts.tabStops)) strXmlTabStops = el("a:tabLst", null, raw(opts.tabStops.map((stop) => voidEl("a:tab", {
1798
+ pos: inch2Emu(mapStated(stop.position, (position) => position) ?? 1),
1799
+ algn: stop.alignment ? checkEnumOrWarn(stop.alignment, TEXT_TAB_ALIGN_TYPES, "text/invalid-tab-alignment", "tab stop `alignment`") : "l"
1800
+ })).join("")));
1801
+ paragraphPropXml += ">" + strXmlLnSpc + strXmlParaSpc + strXmlBulletColor + strXmlBullet + strXmlTabStops;
1802
+ if (isDefault) paragraphPropXml += genXmlTextRunProperties(opts, true);
1803
+ paragraphPropXml += "</" + tag + ">";
1804
+ }
1805
+ return paragraphPropXml;
1806
+ }
1807
+ /**
1808
+ * Generate XML Text Run Properties (`a:rPr`)
1809
+ * @param {ObjectOptions|TextPropsOptions} opts - text options
1810
+ * @param {boolean} isDefault - whether these are the default text run properties
1811
+ * @return {string} XML
1812
+ */
1813
+ function genXmlTextRunProperties(opts, isDefault) {
1814
+ let runProps = "";
1815
+ 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;
1818
+ const attrs = {
1819
+ lang: opts.lang ? opts.lang : "en-US",
1820
+ altLang: opts.lang ? "en-US" : null,
1821
+ 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,
1826
+ u: underline,
1827
+ baseline,
1828
+ spc: mapStated(opts.charSpacing, clampCharSpacingSpc) ?? null,
1829
+ kern: opts.charSpacing ? 0 : null,
1830
+ dirty: "0"
1831
+ };
1832
+ 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) {
1834
+ 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
+ rejectEmptyColor(opts.color, "color");
1836
+ if (opts.color) runProps += genXmlColorSelection(solidPaint(opts.color, opts.transparency));
1837
+ 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
+ rejectEmptyColor(opts.highlight, "highlight");
1839
+ 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)));
1842
+ if (opts.fontFace) {
1843
+ runProps += voidEl("a:latin", {
1844
+ typeface: opts.fontFace,
1845
+ pitchFamily: "34",
1846
+ charset: "0"
1847
+ });
1848
+ if (opts.fontFaceEA) runProps += voidEl("a:ea", { typeface: opts.fontFaceEA });
1849
+ runProps += voidEl("a:cs", { typeface: opts.fontFace });
1850
+ }
1851
+ }
1852
+ if (opts.hyperlink) {
1853
+ try {
1854
+ validateHyperlink(opts.hyperlink, "text");
1855
+ } catch (err) {
1856
+ throw new InternalError("hyperlink/not-validated", `A run reached the emitter with a hyperlink its definer should have refused: ${err.message}`);
1857
+ }
1858
+ if (opts.hyperlink.url || opts.hyperlink.slide) {
1859
+ const link = opts.hyperlink;
1860
+ const linkAttrs = opts.hyperlink.url ? {
1861
+ "r:id": `rId${link._rId}`,
1862
+ invalidUrl: "",
1863
+ action: "",
1864
+ tgtFrame: "",
1865
+ tooltip: opts.hyperlink.tooltip ?? "",
1866
+ history: "1",
1867
+ highlightClick: "0",
1868
+ endSnd: "0"
1869
+ } : {
1870
+ "r:id": `rId${link._rId}`,
1871
+ action: "ppaction://hlinksldjump",
1872
+ tooltip: opts.hyperlink.tooltip ?? ""
1873
+ };
1874
+ runProps += elOrVoid("a:hlinkClick", linkAttrs, opts.color ? HLINK_TEXT_COLOR_EXT : "");
1875
+ }
1876
+ }
1877
+ return el(runPropsTag, attrs, raw(runProps));
1878
+ }
1879
+ /**
1880
+ * Build textBody text runs [`<a:r></a:r>`] for paragraphs [`<a:p>`]
1881
+ * @param {TextProps} textObj - Text object
1882
+ * @return {string} XML string
1883
+ */
1884
+ function genXmlTextRun(textObj) {
1885
+ if (textObj.text === void 0 || textObj.text === null) return "";
1886
+ return el("a:r", null, [raw(genXmlTextRunProperties(textObj.options ?? {}, false)), raw(el("a:t", null, String(textObj.text)))]);
1887
+ }
1888
+ /**
1889
+ * Builds `<a:normAutofit>` with explicit fontScale/lnSpcReduction for "shrink text on overflow"
1890
+ * @param {TextFitShrinkProps} fit - shrink fit options
1891
+ * @return {string} XML string (`<a:normAutofit .../>`)
1892
+ * @see ECMA-376 CT_TextNormAutofit (attributes in 1000ths of a percent)
1893
+ */
1894
+ function genXmlNormAutofit(fit) {
1895
+ const pct = (val, name) => val === void 0 || val === null ? null : percentToFixedPercent(val, "text/invalid-fit-percentage", `fit.${name}`);
1896
+ return voidEl("a:normAutofit", {
1897
+ fontScale: pct(fit.fontScale, "fontScale"),
1898
+ lnSpcReduction: pct(fit.lnSpcReduction, "lnSpcReduction")
1899
+ });
1900
+ }
1901
+ /**
1902
+ * Group the flat run list into paragraphs (lines): a new line starts on a display-math run,
1903
+ * an alignment change, or a bullet, and closes after a breakLine. Returns the per-line run arrays.
1904
+ */
1905
+ function groupRunsIntoLines(arrTextObjects, opts) {
1906
+ const arrLines = [];
1907
+ let arrTexts = [];
1908
+ arrTextObjects.forEach((textObj, idx) => {
1909
+ if (textObj.math && !textObj.inline) {
1910
+ if (arrTexts.length > 0) {
1911
+ arrLines.push(arrTexts);
1912
+ arrTexts = [];
1913
+ }
1914
+ arrLines.push([textObj]);
1915
+ return;
1916
+ }
1917
+ if (arrTexts.length > 0 && (textObj.options.align || opts.align)) {
1918
+ if (textObj.options.align !== arrTextObjects[idx - 1]?.options.align) {
1919
+ arrLines.push(arrTexts);
1920
+ arrTexts = [];
1921
+ }
1922
+ } else if (arrTexts.length > 0 && emitsBulletMarkup(textObj.options.bullet)) {
1923
+ arrLines.push(arrTexts);
1924
+ arrTexts = [];
1925
+ textObj.options.breakLine = false;
1926
+ }
1927
+ arrTexts.push(textObj);
1928
+ if (arrTexts.length > 0 && textObj.options.breakLine) {
1929
+ if (idx + 1 < arrTextObjects.length) {
1930
+ arrLines.push(arrTexts);
1931
+ arrTexts = [];
1932
+ }
1933
+ }
1934
+ if (idx + 1 === arrTextObjects.length) arrLines.push(arrTexts);
1935
+ });
1936
+ return arrLines;
1937
+ }
1938
+ /**
1939
+ * The options a run inherits from its shape — exactly the keys `genXmlTextRunProperties`
1940
+ * reads off a run's bag, in the order it reads them.
1941
+ *
1942
+ * This used to be `Object.entries(opts)` filtered on `!textOptions[key]`, which had two
1943
+ * consequences beyond copying keys (`x`, `w`, …) that mean nothing to an `<a:rPr>`. The
1944
+ * falsy test made an explicit `false`/`0` on the run indistinguishable from silence, so
1945
+ * `addText([{ text: 'a', options: { bold: false } }], { bold: true })` emitted `b="1"` and a
1946
+ * run's `transparency: 0` took the shape's; and because it copied *whatever* was on the
1947
+ * shape's bag, the font-size write-back in step E below leaked one run's size onto every
1948
+ * later run. Naming the keys fixes the first and confines the second.
1949
+ *
1950
+ * Paragraph-level options (`align`, `lineSpacing`, the two margins, …) are inherited
1951
+ * separately in step B, before the `<a:pPr>` is emitted; they are deliberately not here.
1952
+ */
1953
+ const RUN_INHERITABLE_OPTIONS = [
1954
+ "lang",
1955
+ "fontSize",
1956
+ "bold",
1957
+ "italic",
1958
+ "strike",
1959
+ "caps",
1960
+ "underline",
1961
+ "baseline",
1962
+ "subscript",
1963
+ "superscript",
1964
+ "charSpacing",
1965
+ "color",
1966
+ "transparency",
1967
+ "fontFace",
1968
+ "fontFaceEA",
1969
+ "outline",
1970
+ "glow",
1971
+ "highlight",
1972
+ "hyperlink"
1973
+ ];
1974
+ /**
1975
+ * The paragraph-level options a run inherits from its shape -- exactly the keys
1976
+ * `genXmlParagraphProperties` reads off a run's bag, which is where the `<a:pPr>` for the
1977
+ * paragraph that run opens is built.
1978
+ *
1979
+ * A list rather than eight hand-written `setOrClear` calls because it has to be *complete*:
1980
+ * `rtlMode` and `tabStops` were read there and inherited nowhere, and reached a run only because
1981
+ * the bare-string form of `addText` handed the shape's own options object to its run. Once that
1982
+ * stopped, an unlisted key stopped arriving at all -- so the list is now the statement of what a
1983
+ * paragraph inherits, and the place to add to when `genXmlParagraphProperties` learns a new key.
1984
+ *
1985
+ * `bullet` is deliberately not here. It is inherited in STEP 4 of `text-body.ts`, onto the FIRST
1986
+ * text object only, before the line split copies each line's bag -- a rule about which paragraph
1987
+ * gets the shape's bullet, not about whether a paragraph inherits at all.
1988
+ */
1989
+ const PARAGRAPH_INHERITABLE_OPTIONS = ["rtlMode", "tabStops"];
1990
+ /**
1991
+ * Of those, the ones whose zero is a value the caller meant. `paraMarginLeft: 0` is flush with the
1992
+ * frame (and the override that suppresses a bullet's hanging indent) and `paraIndent: 0` likewise,
1993
+ * where `align`/`lineSpacing`/`indentLevel`/the paragraph spacings have no zero worth stating. A
1994
+ * falsy test would silently swap a run's explicit `0` for the shape's value.
1995
+ */
1996
+ const PARAGRAPH_ZERO_IS_STATED = ["paraMarginLeft", "paraIndent"];
1997
+ /**
1998
+ * Whether a value is a number the caller stated and no clamp can use -- `NaN` or `±Infinity`.
1999
+ *
2000
+ * It exists to keep such a value out of {@link PARAGRAPH_FALSY_IS_UNSTATED}'s inheritance. `NaN`
2001
+ * is falsy, so it read as "the caller said nothing" and took the shape's value instead of reaching
2002
+ * the clamp, whose `nonFiniteCode` is the whole point of naming a bad input rather than guessing at
2003
+ * one.
2004
+ */
2005
+ const isUnusableNumber = (value) => typeof value === "number" && !Number.isFinite(value);
2006
+ /**
2007
+ * Whether `<a:spcBef>`/`<a:spcAft>` is written for a paragraph spacing value.
2008
+ *
2009
+ * `0` and a negative value are dropped, as they always were: the schema's `a:spcPts@val` is
2010
+ * unsigned, and `0` is how a caller says "none". A non-finite value is *not* dropped -- it is
2011
+ * passed on so {@link ptToHundredths} refuses it by name, which is what the old guard's comment
2012
+ * claimed to do while `NaN &&` short-circuited it into silence.
2013
+ */
2014
+ const emitsParagraphSpacing = (value) => value !== void 0 && (isUnusableNumber(value) || value > 0);
2015
+ const PARAGRAPH_FALSY_IS_UNSTATED = [
2016
+ "align",
2017
+ "lineSpacing",
2018
+ "lineSpacingMultiple",
2019
+ "indentLevel",
2020
+ "paraSpaceBefore",
2021
+ "paraSpaceAfter"
2022
+ ];
2023
+ /**
2024
+ * Render each grouped line to an `<a:p>` paragraph: paragraph props, inherited run options,
2025
+ * text runs (and inline/display math), and the closing endParaRPr.
2026
+ */
2027
+ function renderTextParagraphsXml(arrLines, slideObj, opts) {
2028
+ let strSlideXml = "";
2029
+ arrLines.forEach((line) => {
2030
+ const firstRun = line[0];
2031
+ if (line.length === 1 && firstRun?.math && !firstRun.inline) {
2032
+ strSlideXml += genXmlMathParagraph(firstRun.math);
2033
+ return;
2034
+ }
2035
+ let reqsClosingFontSize = false;
2036
+ let closingFontSize = opts.fontSize;
2037
+ let paraXml = "";
2038
+ let paragraphPropEmitted = false;
2039
+ line.forEach((textObj, idx) => {
2040
+ textObj.options._lineIdx = idx;
2041
+ if (idx > 0 && textObj.options.softBreakBefore) paraXml += voidEl("a:br");
2042
+ const paraOptions = textObj.options;
2043
+ const paraShapeOptions = opts;
2044
+ for (const key of PARAGRAPH_FALSY_IS_UNSTATED) {
2045
+ const stated = paraOptions[key];
2046
+ setOrClear(paraOptions, key, isUnusableNumber(stated) ? stated : stated || paraShapeOptions[key]);
2047
+ }
2048
+ for (const key of [...PARAGRAPH_ZERO_IS_STATED, ...PARAGRAPH_INHERITABLE_OPTIONS]) setOrClear(paraOptions, key, paraOptions[key] ?? paraShapeOptions[key]);
2049
+ if (!paragraphPropEmitted) {
2050
+ paragraphPropEmitted = true;
2051
+ paraXml += genXmlParagraphProperties(textObj, false).replace("<a:pPr></a:pPr>", "");
2052
+ }
2053
+ const textOptions = textObj.options;
2054
+ const shapeOptions = opts;
2055
+ RUN_INHERITABLE_OPTIONS.forEach((key) => {
2056
+ if (key === "color" && textObj.options.hyperlink) return;
2057
+ setOrClear(textOptions, key, textOptions[key] ?? shapeOptions[key]);
2058
+ });
2059
+ let _textRunObj = textObj;
2060
+ if (idx === 0 && emitsBulletMarkup(line[0]?.options.bullet) && typeof textObj.text === "string") {
2061
+ const _stripped = textObj.text.replace(/^[\u2022\u25E6\u25AA\u25AB\u25CF\u25CB\u2023\u2043\u2219]\s*/, "");
2062
+ if (_stripped !== textObj.text) _textRunObj = {
2063
+ text: _stripped,
2064
+ options: textObj.options
2065
+ };
2066
+ }
2067
+ const isEmptyBreakArtifact = _textRunObj.text === "" && line.length > 1;
2068
+ if (_textRunObj.math) paraXml += genXmlInlineMath(_textRunObj.math);
2069
+ else if (!isEmptyBreakArtifact) paraXml += genXmlTextRun(_textRunObj);
2070
+ if (!textObj.text && opts.fontSize || textObj.options.fontSize) {
2071
+ reqsClosingFontSize = true;
2072
+ closingFontSize = closingFontSize ?? textObj.options.fontSize;
2073
+ }
2074
+ });
2075
+ const sizedAttrs = () => ({
2076
+ lang: opts.lang || "en-US",
2077
+ sz: closingFontSize ? clampFontSizeSz(closingFontSize) : null,
2078
+ dirty: "0"
2079
+ });
2080
+ if (slideObj._type === "tablecell" && (closingFontSize || opts.fontFace)) {
2081
+ if (opts.fontFace) paraXml += el("a:endParaRPr", sizedAttrs(), [
2082
+ raw(voidEl("a:latin", {
2083
+ typeface: opts.fontFace,
2084
+ charset: "0"
2085
+ })),
2086
+ opts.fontFaceEA ? raw(voidEl("a:ea", { typeface: opts.fontFaceEA })) : null,
2087
+ raw(voidEl("a:cs", { typeface: opts.fontFace }))
2088
+ ]);
2089
+ else paraXml += voidEl("a:endParaRPr", sizedAttrs());
2090
+ } else if (reqsClosingFontSize) paraXml += voidEl("a:endParaRPr", sizedAttrs());
2091
+ else paraXml += voidEl("a:endParaRPr", {
2092
+ lang: opts.lang || "en-US",
2093
+ dirty: "0"
2094
+ });
2095
+ strSlideXml += el("a:p", null, raw(paraXml));
2096
+ });
2097
+ return strSlideXml;
2098
+ }
2099
+ //#endregion
2100
+ //#region src/gen/drawingml/geometry.ts
2101
+ /**
2102
+ * ts-pptx: DrawingML geometry
2103
+ *
2104
+ * Emit `<a:prstGeom>` (preset shapes with their adjustment guides) and
2105
+ * `<a:custGeom>` (freeform paths built from the `points` DSL). Shared by the
2106
+ * shape and image code paths so path/preset emission stays in one place.
2107
+ */
2108
+ /**
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.
2111
+ */
2112
+ const SPACE_BEFORE_SLASH = { closePrefix: " " };
2113
+ /**
2114
+ * The path nodes inside `<a:cubicBezTo>`/`<a:quadBezTo>` are newline-and-tab separated,
2115
+ * an artifact of the source indentation these were originally written with. It reaches
2116
+ * the emitted bytes, so it is described here rather than left to leak from a literal.
2117
+ */
2118
+ const BEZ_INDENT = {
2119
+ childPrefix: "\n ",
2120
+ closePrefix: "\n "
2121
+ };
2122
+ /**
2123
+ * 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
2125
+ * unrecognized leading token is always a caller mistake, not a newer dialect.
2126
+ *
2127
+ * Only the operation is checked. The operands are deliberately left uninterpreted:
2128
+ * 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
2130
+ * is exactly what this passthrough exists to avoid.
2131
+ */
2132
+ const GEOM_GUIDE_OPS = /* @__PURE__ */ new Set([
2133
+ "*/",
2134
+ "+-",
2135
+ "+/",
2136
+ "?:",
2137
+ "abs",
2138
+ "at2",
2139
+ "cat2",
2140
+ "cos",
2141
+ "max",
2142
+ "min",
2143
+ "mod",
2144
+ "pin",
2145
+ "sat2",
2146
+ "sin",
2147
+ "sqrt",
2148
+ "tan",
2149
+ "val"
2150
+ ]);
2151
+ /**
2152
+ * Emit an `<a:prstGeom>` for a preset shape, including any adjust values (`<a:avLst>`).
2153
+ * Shared by the shape and image code paths so that geometry + adjust handling stays in one place.
2154
+ * @param {string} shapeName - preset geometry name (e.g. `rect`, `ellipse`, `roundRect`, `hexagon`)
2155
+ * @param {ObjectOptions} options - object options carrying optional `rectRadius`/`angleRange`/`arcThicknessRatio`
2156
+ * @param {number} cx - shape width (EMU), used to scale `rectRadius`
2157
+ * @param {number} cy - shape height (EMU), used to scale `rectRadius`
2158
+ * @return {string} `<a:prstGeom>` XML
2159
+ */
2160
+ const RECT_RADIUS_ADJ1_SHAPES = /* @__PURE__ */ new Set(["round2SameRect", "round2DiagRect"]);
2161
+ /**
2162
+ * Reject a preset PowerPoint can't parse.
2163
+ *
2164
+ * An unknown `prst` value corrupts the package: PowerPoint shows the "needs
2165
+ * 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
2168
+ * their own typo at the API boundary rather than at serialization time.
2169
+ * @param {string} shapeName - the resolved preset geometry name
2170
+ * @throws {InvalidOptionError} `shape/unknown-preset` when the name is not a preset
2171
+ */
2172
+ function assertKnownPreset(shapeName) {
2173
+ if (VALID_SHAPE_PRESETS.has(shapeName)) return;
2174
+ throw new InvalidOptionError("shape/unknown-preset", `Invalid shape "${String(shapeName)}"! Use a value from \`ShapeType.*\` (e.g. \`ShapeType.rect\`). PowerPoint can't render unknown preset geometries and will drop the shape during repair.`);
2175
+ }
2176
+ /**
2177
+ * A finite adjustment guide is emitted verbatim, however far outside the preset's own handle range
2178
+ * it falls. This is a decision, not an omission.
2179
+ *
2180
+ * Each preset defines its handle's bounds in its own geometry definition (the `ahXY`/`ahPolar`
2181
+ * min/max formulas of [MS-ODRAWXML]'s preset shape definitions), and nothing in ECMA-376 states
2182
+ * them -- `a:gd/@fmla` is `xsd:string`, so no schema check can see an out-of-range value either.
2183
+ * Clamping would therefore mean carrying an invented table, and many of those bounds are formulas
2184
+ * over the shape's own width and height rather than constants.
2185
+ *
2186
+ * It would also buy nothing. Desktop PowerPoint stores an out-of-range guide verbatim -- it neither
2187
+ * repairs the package on open nor rewrites the value on re-save -- and the preset's own formula
2188
+ * pins it at render time, so the shape it paints is the shape it paints at the bound. Verified by
2189
+ * exporting the slides to PNG and comparing pixels: `roundRect` at `adj` 50000, 60000 and 266667
2190
+ * rasterize identically, as do `blockArc` at `adj3` 50000 and 5000000, while two *in*-range values
2191
+ * of the same guide do not. `pnpm run test:com` carries that as a standing case.
2192
+ *
2193
+ * What IS refused is a guide PowerPoint cannot evaluate at all: a non-finite value, and a
2194
+ * `rectRadius` with no side to divide by. Those write `val NaN` / `val Infinity`, which is a token
2195
+ * outside the formula grammar rather than a number outside a range.
2196
+ */
2197
+ function genXmlPresetGeom(shapeName, options, cx, cy) {
2198
+ assertKnownPreset(shapeName);
2199
+ let avLst = "";
2200
+ const emittedAdjNames = /* @__PURE__ */ new Set();
2201
+ const emitGuide = (name, fmlaVal) => {
2202
+ avLst += voidEl("a:gd", {
2203
+ name,
2204
+ fmla: `val ${fmlaVal}`
2205
+ });
2206
+ emittedAdjNames.add(name);
2207
+ };
2208
+ if (options.rectRadius !== void 0) {
2209
+ const shorterSideEmu = Math.min(cx, cy);
2210
+ if (!Number.isFinite(options.rectRadius)) warn("geometry/invalid-shape-adjust", `rectRadius ${String(options.rectRadius)} is not a finite number and was ignored.`);
2211
+ else if (!(shorterSideEmu > 0)) warn("shape/degenerate-extent", `rectRadius cannot be resolved against a ${cx}x${cy} EMU extent (the shape has no size on one axis); the preset's own corner radius is kept.`);
2212
+ else {
2213
+ const adjVal = Math.round(options.rectRadius * EMU_PER_INCH * PERCENT_SCALE / shorterSideEmu);
2214
+ if (RECT_RADIUS_ADJ1_SHAPES.has(shapeName)) {
2215
+ emitGuide("adj1", adjVal);
2216
+ emitGuide("adj2", 0);
2217
+ } else emitGuide("adj", adjVal);
2218
+ }
2219
+ } else if (options.angleRange !== void 0) {
2220
+ for (let i = 0; i < 2; i++) {
2221
+ const angle = options.angleRange[i] ?? 0;
2222
+ emitGuide(`adj${i + 1}`, convertAngleUnits(angle, `angleRange[${i}]`));
2223
+ }
2224
+ if (options.arcThicknessRatio !== void 0) {
2225
+ if (Number.isFinite(options.arcThicknessRatio)) emitGuide("adj3", Math.round(options.arcThicknessRatio * (PERCENT_SCALE / 2)));
2226
+ else warn("geometry/invalid-shape-adjust", `arcThicknessRatio ${String(options.arcThicknessRatio)} is not a finite number and was ignored.`);
2227
+ }
2228
+ }
2229
+ if (options.shapeAdjust) (Array.isArray(options.shapeAdjust) ? options.shapeAdjust : [options.shapeAdjust]).forEach((adj) => {
2230
+ if (!adj || typeof adj.name !== "string" || adj.name.length === 0 || typeof adj.value !== "number" || !Number.isFinite(adj.value)) {
2231
+ warn("geometry/invalid-shape-adjust", `shapeAdjust entry ${JSON.stringify(adj)} is invalid (needs { name:string, value:number }) and was ignored.`);
2232
+ return;
2233
+ }
2234
+ if (emittedAdjNames.has(adj.name)) {
2235
+ warn("geometry/shape-adjust-overridden", `shapeAdjust "${adj.name}" was ignored because rectRadius/angleRange already set that handle.`);
2236
+ return;
2237
+ }
2238
+ emitGuide(adj.name, Math.round(adj.value * PERCENT_SCALE));
2239
+ });
2240
+ return el("a:prstGeom", { prst: shapeName }, raw(el("a:avLst", null, raw(avLst))));
2241
+ }
2242
+ /**
2243
+ * Narrow a freeform path node to an arc segment. An arc is the one curve node with no end
2244
+ * point, so it must be split off before the remaining curve nodes can be read for x/y.
2245
+ */
2246
+ function isArcPoint(point) {
2247
+ return "curve" in point && point.curve.type === "arc";
2248
+ }
2249
+ /**
2250
+ * Emit an `<a:custGeom>` for a freeform path built from `points`, plus any caller-supplied
2251
+ * connection sites (`connectionSites` → `<a:cxnLst>`), adjust handles (`adjustHandles` →
2252
+ * `<a:ahLst>`) and guide formulas (`guides` → `<a:gdLst>`) that make the shape connectable and
2253
+ * editable. Shared by the shape and image code paths so that path emission stays in one place.
2254
+ * 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).
2257
+ * @param {ObjectOptions} options - object options carrying `points`/`guides`/`connectionSites`/`adjustHandles`
2258
+ * @param {number} cx - object width (EMU), used as the path viewport width
2259
+ * @param {number} cy - object height (EMU), used as the path viewport height
2260
+ * @param {PresLayout} layout - presentation layout used to resolve point coordinates to EMU
2261
+ * @return {string} `<a:custGeom>` XML
2262
+ */
2263
+ function genXmlCustGeom(options, cx, cy, layout) {
2264
+ const points = options.points;
2265
+ /**
2266
+ * Resolve an `ST_AdjCoordinate` value: a number / unit-suffixed `Coord` → EMU (like `points`),
2267
+ * but a token that starts with a letter/underscore is a guide-name reference emitted verbatim.
2268
+ * `undefined` → `undefined`, so `el`/`voidEl` drop the (optional) attribute entirely.
2269
+ */
2270
+ const adjCoord = (v, axis) => {
2271
+ if (v === void 0 || v === null) return void 0;
2272
+ if (typeof v === "string" && /^[A-Za-z_]/.test(v.trim())) return v;
2273
+ return String(getSmartParseNumber(v, axis, layout));
2274
+ };
2275
+ /** `<a:pt>` in the object's own EMU space. */
2276
+ const pt = (x, y) => voidEl("a:pt", {
2277
+ x: getSmartParseNumber(x, "X", layout),
2278
+ y: getSmartParseNumber(y, "Y", layout)
2279
+ }, SPACE_BEFORE_SLASH);
2280
+ const nodes = [];
2281
+ points?.forEach((point, i) => {
2282
+ 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.");
2284
+ 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")
2289
+ }, SPACE_BEFORE_SLASH));
2290
+ } else if ("curve" in point) switch (point.curve.type) {
2291
+ case "cubic":
2292
+ nodes.push(el("a:cubicBezTo", null, [
2293
+ pt(point.curve.x1, point.curve.y1),
2294
+ pt(point.curve.x2, point.curve.y2),
2295
+ pt(point.x, point.y)
2296
+ ].map(raw), BEZ_INDENT));
2297
+ break;
2298
+ case "quadratic": nodes.push(el("a:quadBezTo", null, [pt(point.curve.x1, point.curve.y1), pt(point.x, point.y)].map(raw), BEZ_INDENT));
2299
+ }
2300
+ else if ("close" in point) nodes.push(voidEl("a:close", null, SPACE_BEFORE_SLASH));
2301
+ else if (point.moveTo || i === 0) nodes.push(el("a:moveTo", null, raw(pt(point.x, point.y))));
2302
+ else nodes.push(el("a:lnTo", null, raw(pt(point.x, point.y))));
2303
+ });
2304
+ let gdLst = el("a:gdLst");
2305
+ if (options.guides?.length) {
2306
+ const guideEls = [];
2307
+ options.guides.forEach((g) => {
2308
+ if (!g || typeof g.name !== "string" || g.name.length === 0 || typeof g.formula !== "string" || g.formula.length === 0) {
2309
+ warn("geometry/invalid-guide", `guide entry ${JSON.stringify(g)} is invalid (needs { name:string, formula:string }) and was ignored.`);
2310
+ return;
2311
+ }
2312
+ const op = g.formula.trimStart().split(/\s+/)[0] ?? "";
2313
+ if (!GEOM_GUIDE_OPS.has(op)) {
2314
+ warn("geometry/unknown-guide-operation", `guide "${g.name}" formula ${JSON.stringify(g.formula)} starts with an unknown operation "${op}" (expected one of ${[...GEOM_GUIDE_OPS].join(" ")}) and was ignored.`);
2315
+ return;
2316
+ }
2317
+ guideEls.push(voidEl("a:gd", {
2318
+ name: g.name,
2319
+ fmla: g.formula
2320
+ }));
2321
+ });
2322
+ if (guideEls.length) gdLst = el("a:gdLst", null, guideEls.map(raw));
2323
+ }
2324
+ let cxnLst = el("a:cxnLst");
2325
+ if (options.connectionSites?.length) {
2326
+ const cxnEls = [];
2327
+ options.connectionSites.forEach((c) => {
2328
+ if (!c || typeof c.ang !== "number" || !Number.isFinite(c.ang)) {
2329
+ warn("geometry/invalid-connection-site", `connectionSite entry ${JSON.stringify(c)} is invalid (needs a finite \`ang\` in degrees) and was ignored.`);
2330
+ return;
2331
+ }
2332
+ const pos = voidEl("a:pos", {
2333
+ x: adjCoord(c.x, "X"),
2334
+ y: adjCoord(c.y, "Y")
2335
+ });
2336
+ cxnEls.push(el("a:cxn", { ang: convertAngleUnits(c.ang, "connectionSite ang") }, raw(pos)));
2337
+ });
2338
+ if (cxnEls.length) cxnLst = el("a:cxnLst", null, cxnEls.map(raw));
2339
+ }
2340
+ let ahLst = voidEl("a:ahLst", null, SPACE_BEFORE_SLASH);
2341
+ if (options.adjustHandles?.length) ahLst = el("a:ahLst", null, options.adjustHandles.map((h) => {
2342
+ const pos = raw(voidEl("a:pos", {
2343
+ x: adjCoord(h.x, "X"),
2344
+ y: adjCoord(h.y, "Y")
2345
+ }));
2346
+ if ("gdRefR" in h || "minR" in h || "maxR" in h || "gdRefAng" in h || "minAng" in h || "maxAng" in h) return el("a:ahPolar", {
2347
+ gdRefR: h.gdRefR,
2348
+ minR: adjCoord(h.minR, "X"),
2349
+ maxR: adjCoord(h.maxR, "X"),
2350
+ gdRefAng: h.gdRefAng,
2351
+ minAng: h.minAng == null ? void 0 : convertAngleUnits(h.minAng, "adjustHandle minAng"),
2352
+ maxAng: h.maxAng == null ? void 0 : convertAngleUnits(h.maxAng, "adjustHandle maxAng")
2353
+ }, pos);
2354
+ const xy = h;
2355
+ return el("a:ahXY", {
2356
+ gdRefX: xy.gdRefX,
2357
+ minX: adjCoord(xy.minX, "X"),
2358
+ maxX: adjCoord(xy.maxX, "X"),
2359
+ gdRefY: xy.gdRefY,
2360
+ minY: adjCoord(xy.minY, "Y"),
2361
+ maxY: adjCoord(xy.maxY, "Y")
2362
+ }, pos);
2363
+ }).map(raw));
2364
+ return el("a:custGeom", null, [
2365
+ voidEl("a:avLst", null, SPACE_BEFORE_SLASH),
2366
+ gdLst,
2367
+ ahLst,
2368
+ cxnLst,
2369
+ voidEl("a:rect", {
2370
+ l: "l",
2371
+ t: "t",
2372
+ r: "r",
2373
+ b: "b"
2374
+ }, SPACE_BEFORE_SLASH),
2375
+ el("a:pathLst", null, raw(el("a:path", {
2376
+ w: cx,
2377
+ h: cy
2378
+ }, nodes.map(raw))))
2379
+ ].map(raw));
2380
+ }
2381
+ /**
2382
+ * `<a:prstGeom prst="rect"><a:avLst/></a:prstGeom>` — the unadjusted rectangle every emitter
2383
+ * that draws a plain box writes, seven of them by hand.
2384
+ *
2385
+ * `fmt` carries the caller's `openPrefix`, because indentation before this element is
2386
+ * byte-significant on two of those paths.
2387
+ */
2388
+ function prstGeomRect(fmt) {
2389
+ return el("a:prstGeom", { prst: "rect" }, raw(voidEl("a:avLst")), fmt);
2390
+ }
2391
+ //#endregion
2392
+ //#region src/gen/slide/objects/shared.ts
2393
+ /**
2394
+ * The `<p:cNvPr>` every shape renderer writes: self-closing for most shape kinds, paired around a
2395
+ * hyperlink or media-action child for the others.
2396
+ *
2397
+ * Both `name` and `descr` are escaped by the element builder and nowhere before it: a slide object
2398
+ * stores its `objectName` as the caller wrote it, so lookups by name and `slide.objects` compare and
2399
+ * report the caller's own spelling. Each caller used to append its own `/>` or `>`, child and close
2400
+ * tag to an open tag this module returned.
2401
+ * @param id - the shape's `<p:cNvPr>` id, unique slide-wide
2402
+ * @param name - the object's `objectName`, raw
2403
+ * @param descr - alt text, raw
2404
+ * @param children - already-serialized children; absent means self-closing, `''` means paired and empty
2405
+ * @param fmt - byte-significant whitespace around the element
2406
+ */
2407
+ function cNvPrEl(id, name, descr, children, fmt) {
2408
+ const attrs = {
2409
+ id,
2410
+ name: name ?? "",
2411
+ descr
2412
+ };
2413
+ return children === void 0 ? voidEl("p:cNvPr", attrs, fmt) : el("p:cNvPr", attrs, raw(children), fmt);
2414
+ }
2415
+ /**
2416
+ * The `<a:hlinkClick>` child of a shape's `<p:cNvPr>`: a URL link, a jump to another slide, or a
2417
+ * slide-show action. Shared by the text and image renderers, which emitted identical copies.
2418
+ *
2419
+ * `<p:cNvPr>` holds at most one, so one destination is written, in that order of precedence. The
2420
+ * three used to be concatenated, which put two in one element for a link stating both `url` and
2421
+ * `slide`; `validateHyperlink` now refuses that link before it gets here.
2422
+ *
2423
+ * NOTE: the tooltip is passed through UNESCAPED and escaped once by the element builder. Escaping
2424
+ * it here as well would emit `&amp;amp;` for a tooltip containing `&`.
2425
+ * @param link - the shape's hyperlink, if any
2426
+ * @returns zero or one `<a:hlinkClick>` element
2427
+ */
2428
+ function cNvPrHyperlink(link) {
2429
+ if (!link) return "";
2430
+ const tooltip = link.tooltip ?? "";
2431
+ if (link.url) return voidEl("a:hlinkClick", {
2432
+ "r:id": `rId${link._rId}`,
2433
+ tooltip
2434
+ });
2435
+ if (link.slide) return voidEl("a:hlinkClick", {
2436
+ "r:id": `rId${link._rId}`,
2437
+ tooltip,
2438
+ action: "ppaction://hlinksldjump"
2439
+ });
2440
+ if (link.action) return voidEl("a:hlinkClick", {
2441
+ "r:id": "",
2442
+ tooltip,
2443
+ action: `ppaction://hlinkshowjump?jump=${link.action}`
2444
+ });
2445
+ return "";
2446
+ }
2447
+ /**
2448
+ * A shape outline: `<a:ln>` with its fill, dash pattern and arrow ends.
2449
+ *
2450
+ * The text, connector and image renderers each carried a byte-identical copy of this block; they
2451
+ * now share one. Attribute and child order are byte-significant and preserved as written.
2452
+ *
2453
+ * FUTURE: arrow-size support via the `w`/`len` attrs on headEnd/tailEnd
2454
+ * (e.g. `<a:headEnd type="arrow" w="lg" len="lg"/>`; each is 'sm'|'med'|'lg', a 3x3 grid)
2455
+ * @param ln - the shape's line properties
2456
+ * @returns the `<a:ln>` element
2457
+ */
2458
+ function genXmlShapeLine(ln) {
2459
+ return el("a:ln", {
2460
+ w: mapStated(ln.width, (width) => lineWidthToEmu(width, "line: width")) ?? null,
2461
+ cap: ln.cap ? createLineCap(ln.cap) : null
2462
+ }, [
2463
+ raw(genXmlLineFill(ln)),
2464
+ ln.dashType ? raw(voidEl("a:prstDash", { val: resolveDash(ln.dashType, "solid", "line: dashType") })) : null,
2465
+ raw(lineEndEl("a:headEnd", ln.beginArrowType, "line: beginArrowType")),
2466
+ raw(lineEndEl("a:tailEnd", ln.endArrowType, "line: endArrowType"))
2467
+ ]);
2468
+ }
2469
+ /**
2470
+ * An `<a:off>`/`<a:ext>` transform, under whichever wrapper the context calls for: `p:xfrm` on a
2471
+ * `<p:graphicFrame>`, `a:xfrm` inside a `<p:spPr>`. Fourteen sites across the shape, chart,
2472
+ * connector, image, media, notes-master, text and zoom emitters wrote the same six-line pair;
2473
+ * `attrs` is what the last of them needed (a shape's `rot`/`flipH`/`flipV` ride on the `a:xfrm`
2474
+ * itself, not on its children).
2475
+ * @param tag - `p:xfrm` or `a:xfrm`
2476
+ * @param frame - the box, in EMU
2477
+ * @param attrs - attributes on the transform element itself, e.g. rotation and flips
2478
+ * @param fmt - byte-significant layout, where the caller's part indents
2479
+ */
2480
+ function xfrmEl(tag, frame, attrs = null, fmt) {
2481
+ return el(tag, attrs, [raw(voidEl("a:off", {
2482
+ x: frame.x,
2483
+ y: frame.y
2484
+ })), raw(voidEl("a:ext", {
2485
+ cx: frame.cx,
2486
+ cy: frame.cy
2487
+ }))], fmt);
2488
+ }
2489
+ /**
2490
+ * A group's `<a:xfrm>`: the box, then an IDENTITY child coordinate space (`chOff`/`chExt` equal
2491
+ * to `off`/`ext`).
2492
+ *
2493
+ * The identity is the contract `docs/groups.md` and the measured-fit solver both rest on — a
2494
+ * group never scales its children, so a grouped shape's authored size is its rendered size.
2495
+ * Three group emitters spelled the four-child block out.
2496
+ * @param frame - the box, in EMU
2497
+ * @param attrs - attributes on the `a:xfrm` itself
2498
+ */
2499
+ function grpXfrmEl(frame, attrs = null) {
2500
+ return el("a:xfrm", attrs, [
2501
+ raw(voidEl("a:off", {
2502
+ x: frame.x,
2503
+ y: frame.y
2504
+ })),
2505
+ raw(voidEl("a:ext", {
2506
+ cx: frame.cx,
2507
+ cy: frame.cy
2508
+ })),
2509
+ raw(voidEl("a:chOff", {
2510
+ x: frame.x,
2511
+ y: frame.y
2512
+ })),
2513
+ raw(voidEl("a:chExt", {
2514
+ cx: frame.cx,
2515
+ cy: frame.cy
2516
+ }))
2517
+ ]);
2518
+ }
2519
+ /**
2520
+ * A part's `<p:spTree>`: the implicit top-level group's non-visual properties (the reserved
2521
+ * `cNvPr id="1"`) and an identity group transform, zeroed, then the shapes.
2522
+ *
2523
+ * This is the part's built-in root group, not a user-authored `addGroup`, hence the zeroed frame.
2524
+ * Every slide, layout, master, notes slide and notes master opens its tree with exactly these
2525
+ * bytes, and the slide emitter and the notes emitter each used to write them out.
2526
+ * @param children - the shapes, already serialized, in document order
2527
+ */
2528
+ function spTreeEl(children) {
2529
+ return el("p:spTree", null, [
2530
+ raw(el("p:nvGrpSpPr", null, [
2531
+ raw(voidEl("p:cNvPr", {
2532
+ id: 1,
2533
+ name: ""
2534
+ })),
2535
+ raw(voidEl("p:cNvGrpSpPr")),
2536
+ raw(voidEl("p:nvPr"))
2537
+ ])),
2538
+ raw(el("p:grpSpPr", null, raw(grpXfrmEl({
2539
+ x: 0,
2540
+ y: 0,
2541
+ cx: 0,
2542
+ cy: 0
2543
+ })))),
2544
+ ...children.map((child) => raw(child))
2545
+ ]);
2546
+ }
2547
+ /**
2548
+ * A `<p:graphicFrame>`: its non-visual properties, its transform, and the `<a:graphic>` /
2549
+ * `<a:graphicData>` envelope around a payload.
2550
+ *
2551
+ * The child order is the point. `CT_GraphicalObjectFrame` sequences
2552
+ * `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
2554
+ * build one (chart, table, OLE, zoom, 3D model) each had to be right about separately.
2555
+ *
2556
+ * `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
2558
+ * `<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
2560
+ * differences is deliberate and documented where it is made.
2561
+ *
2562
+ * @param opts.nvGraphicFramePr - the already-serialized `<p:nvGraphicFramePr>`
2563
+ * @param opts.frame - the frame's slide-absolute box, in EMU
2564
+ * @param opts.uri - the `<a:graphicData>` payload namespace
2565
+ * @param opts.payload - the already-serialized `<a:graphicData>` content
2566
+ * @param opts.fmt - per-element byte layout; `graphicAttrs` adds attributes to `<a:graphic>`
2567
+ * (the chart frame alone redeclares `xmlns:a` there, as PowerPoint writes it)
2568
+ */
2569
+ function graphicFrameEl(opts) {
2570
+ const { nvGraphicFramePr, frame, uri, payload, fmt } = opts;
2571
+ return el("p:graphicFrame", null, [
2572
+ raw(nvGraphicFramePr),
2573
+ raw(xfrmEl("p:xfrm", frame, null, fmt?.xfrm)),
2574
+ raw(el("a:graphic", fmt?.graphicAttrs ?? null, raw(el("a:graphicData", { uri }, raw(payload), fmt?.graphicData)), fmt?.graphic))
2575
+ ]);
2576
+ }
2577
+ /**
2578
+ * The `<a:picLocks>` set PowerPoint fixes on an `mc:Fallback` preview picture.
2579
+ *
2580
+ * Fixed rather than taken from the caller's `objectLock`, and that is true of every construct
2581
+ * 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
2583
+ * two element types also accept different flags, so folding a `graphicFrameLocks` set onto a
2584
+ * `picLocks` would warn about every flag they do not share. Routed through `genXmlObjectLock` so
2585
+ * attribute order comes from `PICTURE_LOCK_ATTRS` (`gen/drawingml/locks.ts`) rather than from a literal, and a flag
2586
+ * added to the table lands in the right place.
2587
+ *
2588
+ * A 3D model spreads `noCrop` on top: it is reframed by its camera, never by cropping the cached
2589
+ * raster.
2590
+ */
2591
+ const FALLBACK_PICTURE_LOCKS = Object.freeze({
2592
+ noGrp: true,
2593
+ noRot: true,
2594
+ noChangeAspect: true,
2595
+ noMove: true,
2596
+ noResize: true,
2597
+ noEditPoints: true,
2598
+ noAdjustHandles: true,
2599
+ noChangeArrowheads: true,
2600
+ noChangeShapeType: true
2601
+ });
2602
+ function previewPicBody(previewRid, frame, outline = false, prefix = { ns: "p" }) {
2603
+ const attrs = prefix.nsUri === void 0 ? null : { [`xmlns:${prefix.ns}`]: prefix.nsUri };
2604
+ return el(`${prefix.ns}:blipFill`, attrs, [raw(voidEl("a:blip", { "r:embed": `rId${previewRid}` })), raw(STRETCH_FILL_RECT)]) + el(`${prefix.ns}:spPr`, attrs, [
2605
+ raw(xfrmEl("a:xfrm", frame)),
2606
+ raw(prstGeomRect()),
2607
+ outline ? raw(el("a:ln", { w: "3175" }, raw(el("a:solidFill", null, raw(voidEl("a:prstClr", { val: "ltGray" })))))) : null
2608
+ ]);
2609
+ }
2610
+ //#endregion
2611
+ //#region src/gen/opc/part-paths.ts
2612
+ /**
2613
+ * Where the parts of a written package live.
2614
+ *
2615
+ * Every part is named at least three times: the assembler writes it, `[Content_Types].xml`
2616
+ * 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
2618
+ * 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
2620
+ * PowerPoint repair, and so is a relationship pointing at a part nobody wrote.
2621
+ *
2622
+ * 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 —
2624
+ * {@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
2626
+ * and then transformed, never re-typed in a second dialect.
2627
+ *
2628
+ * Indices are 1-based throughout, matching the OPC naming rather than the arrays that drive it.
2629
+ */
2630
+ /** `ppt/presentation.xml`, the package's root presentation part. */
2631
+ const PRESENTATION_PATH = "ppt/presentation.xml";
2632
+ /** `ppt/slideMasters/slideMaster1.xml`. Exactly one master part is written. */
2633
+ const SLIDE_MASTER_PATH = "ppt/slideMasters/slideMaster1.xml";
2634
+ /** `ppt/notesMasters/notesMaster1.xml`. Exactly one notes-master part is written. */
2635
+ const NOTES_MASTER_PATH = "ppt/notesMasters/notesMaster1.xml";
2636
+ /** The `n`-th slide part. */
2637
+ function slidePath(n) {
2638
+ return `ppt/slides/slide${n}.xml`;
2639
+ }
2640
+ /** The `n`-th slide-layout part. */
2641
+ function slideLayoutPath(n) {
2642
+ return `ppt/slideLayouts/slideLayout${n}.xml`;
2643
+ }
2644
+ /**
2645
+ * 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.
2647
+ */
2648
+ function notesSlidePath(n) {
2649
+ return `ppt/notesSlides/notesSlide${n}.xml`;
2650
+ }
2651
+ /**
2652
+ * The comment part for the `n`-th slide. Written only for slides that carry comments, so the
2653
+ * index is a slide number with gaps in it rather than a running count of comment parts.
2654
+ */
2655
+ function commentPath(n) {
2656
+ return `ppt/comments/comment${n}.xml`;
2657
+ }
2658
+ /** The `n`-th embedded-font part, holding one whole face's raw bytes. */
2659
+ function fontPath(n) {
2660
+ return `ppt/fonts/font${n}.${FONT_DATA_EXTENSION}`;
2661
+ }
2662
+ /**
2663
+ * The `.rels` part that carries `path`'s relationships: `ppt/slides/_rels/slide1.xml.rels`.
2664
+ * A part with no relationships has no such part; this only says where one would go.
2665
+ */
2666
+ function relsPath(path) {
2667
+ const cut = path.lastIndexOf("/");
2668
+ return `${path.slice(0, cut)}/_rels/${path.slice(cut + 1)}.rels`;
2669
+ }
2670
+ /**
2671
+ * `path` as `[Content_Types].xml` spells it in an `Override/@PartName`: the same path with the
2672
+ * leading slash ECMA-376 Part 2 requires of a part name.
2673
+ */
2674
+ function overrideName(path) {
2675
+ return `/${path}`;
2676
+ }
2677
+ /**
2678
+ * `path` as a relationship target stated by a part directly under `ppt/` — presentation.xml,
2679
+ * whose rels reach `slides/slide1.xml` with no traversal at all.
2680
+ */
2681
+ function targetFromPresentation(path) {
2682
+ return path.slice(4);
2683
+ }
2684
+ /**
2685
+ * `path` as a relationship target stated by a part one level below `ppt/` — a slide, a layout,
2686
+ * a notes slide, the master: `../slides/slide1.xml`.
2687
+ */
2688
+ function targetFromPptSubpart(path) {
2689
+ return `../${targetFromPresentation(path)}`;
2690
+ }
2691
+ /**
2692
+ * `path` as a relationship target stated by the part at `fromPath`, where both live one level
2693
+ * below `ppt/`.
2694
+ *
2695
+ * Two parts in the same directory name each other bare, and anything else traverses. That is
2696
+ * PowerPoint's own convention: in an authored deck a slide's rels reach another slide as
2697
+ * `slide3.xml` and its layout as `../slideLayouts/slideLayout7.xml`.
2698
+ *
2699
+ * It exists because the slide-link target used to be spelled `slide${n}.xml` unconditionally, by
2700
+ * 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
2702
+ * part no package has. Deriving the target from the emitting part's own path means the question
2703
+ * is asked once, by the only code that can answer it.
2704
+ */
2705
+ function targetBetweenPptSubparts(fromPath, path) {
2706
+ const dir = fromPath.slice(0, fromPath.lastIndexOf("/") + 1);
2707
+ return path.startsWith(dir) ? path.slice(dir.length) : targetFromPptSubpart(path);
2708
+ }
2709
+ //#endregion
2710
+ //#region src/gen/opc/rels.ts
2711
+ /**
2712
+ * ts-pptx: shared `Relationship` writers.
2713
+ *
2714
+ * A `.rels` part is mostly one-off entries, written where the part that needs
2715
+ * 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
2717
+ * `TargetMode` disagree between two writers does not throw, it produces a link
2718
+ * PowerPoint silently drops.
2719
+ */
2720
+ /**
2721
+ * One `Relationship` entry.
2722
+ *
2723
+ * Three private copies of this used to sit in `gen/opc/root-rels.ts`, `gen/pres/
2724
+ * presentation-rels.ts` and `gen/chart/embed-xlsx.ts`, identical but for whether the caller
2725
+ * passed `rId3` or `3`, with seven more written inline in `gen/slide/`. The attribute set and
2726
+ * its order are byte-significant, so one writer is the only way they stay one fact.
2727
+ *
2728
+ * `target` is passed **unescaped** — `voidEl` escapes it on the way out. `gen/slide/object.ts`
2729
+ * depends on that: it computes the escaped form separately to compare a rel against the bytes
2730
+ * already emitted, and the two escapers must not drift.
2731
+ * @param {string | number} id - the relationship id; a number gets the `rId` prefix
2732
+ * @param {string} type - the relationship type URI
2733
+ * @param {string} target - the target, unescaped (a partname, or a URL with `targetMode`)
2734
+ * @param {object} [opts] - `targetMode` for an external target; `fmt` for byte-layout prefixes
2735
+ * @returns {string} the `Relationship` element
2736
+ */
2737
+ function relationshipEl(id, type, target, opts) {
2738
+ return voidEl("Relationship", {
2739
+ Id: typeof id === "number" ? `rId${id}` : id,
2740
+ Type: type,
2741
+ Target: target,
2742
+ TargetMode: opts?.targetMode
2743
+ }, opts?.fmt);
2744
+ }
2745
+ /**
2746
+ * The `<Relationships>` wrapper around already-serialized entries.
2747
+ *
2748
+ * Only the tag name and its namespace are fixed here; `fmt` is passed straight through, because
2749
+ * the six `.rels` writers each indent differently and every one of those differences is in the
2750
+ * emitted bytes. Normalizing them would be a rewrite of five parts disguised as a cleanup.
2751
+ */
2752
+ function relationshipsEl(rels, fmt) {
2753
+ return el("Relationships", { xmlns: PACKAGE_REL_NS }, rels.map(raw), fmt);
2754
+ }
2755
+ /**
2756
+ * A complete `.rels` part: the XML declaration, a CRLF, then {@link relationshipsEl}.
2757
+ *
2758
+ * The embedded workbook's parts are the exception and use {@link relationshipsEl} directly —
2759
+ * they follow the declaration with nothing, and two of them end with a bare newline.
2760
+ */
2761
+ function relationshipsPart(rels, fmt) {
2762
+ return XML_DECL + "\r\n" + relationshipsEl(rels, fmt);
2763
+ }
2764
+ /**
2765
+ * A `Relationship` to a URL outside the package: the shape of every external
2766
+ * hyperlink, whether it hangs off a slide (`gen/slide/object.ts`) or off a notes
2767
+ * slide (`gen/slide/notes.ts`). `TargetMode="External"` is what makes the target
2768
+ * a URI rather than a partname, so it is not optional.
2769
+ * @param {number} rId - the relationship id, without its `rId` prefix
2770
+ * @param {string} target - the URL, unescaped; `voidEl` escapes it for the attribute
2771
+ * @returns {string} the `Relationship` element
2772
+ */
2773
+ function externalHyperlinkRel(rId, target) {
2774
+ return relationshipEl(rId, HYPERLINK_REL, target, { targetMode: "External" });
2775
+ }
2776
+ //#endregion
2777
+ //#region src/gen/drawingml/list-style.ts
2778
+ /**
2779
+ * ts-pptx: the `<a:lvlNpPr>` list-style level and the theme-font `<a:defRPr>` inside it.
2780
+ *
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`
2784
+ * over one colour, then the `latin`/`ea`/`cs` triple pointing at one theme font family. They
2785
+ * differed in the size, in whether `b`/`i` were carried, and in whether the colour and the
2786
+ * Latin face were overridable, which is what the parameters here are.
2787
+ */
2788
+ /**
2789
+ * The `<a:defRPr>` a list-style level carries: `attrs` (the size, and `b`/`i`/`kern` where the
2790
+ * caller states them), then the fill and the three typeface slots.
2791
+ *
2792
+ * @param font - the theme font family the three slots point at (`mj` = major/heading, `mn` = minor/body)
2793
+ * @param attrs - the run-property attributes, in emission order; `null` values are omitted
2794
+ * @param colorXml - the colour element inside `<a:solidFill>`; defaults to `<a:schemeClr val="tx1"/>`
2795
+ * @param latinXml - the `<a:latin>` element; defaults to the `font` family's own `+xx-lt` token
2796
+ */
2797
+ function themeFontDefRPr(font, attrs, colorXml, latinXml) {
2798
+ return el("a:defRPr", attrs, [
2799
+ raw(el("a:solidFill", null, raw(colorXml ?? voidEl("a:schemeClr", { val: "tx1" })))),
2800
+ raw(latinXml ?? voidEl("a:latin", { typeface: `+${font}-lt` })),
2801
+ raw(voidEl("a:ea", { typeface: `+${font}-ea` })),
2802
+ raw(voidEl("a:cs", { typeface: `+${font}-cs` }))
2803
+ ]);
2804
+ }
2805
+ /**
2806
+ * One `<a:lvlNpPr>`, with {@link LEVEL_PPR_TAIL} appended to the caller's attributes.
2807
+ *
2808
+ * The tail is the fixed `defTabSz`/`rtl`/`eaLnBrk`/`latinLnBrk`/`hangingPunct` run every level
2809
+ * in the deck carries; it sits last because attribute order is byte-significant.
2810
+ * @param level - the 1-based list level (`1` emits `a:lvl1pPr`)
2811
+ * @param attrs - the level's own attributes, ahead of the tail
2812
+ * @param children - the level's children, in schema order
2813
+ */
2814
+ function lvlPPr(level, attrs, children) {
2815
+ return el(`a:lvl${level}pPr`, {
2816
+ ...attrs,
2817
+ ...LEVEL_PPR_TAIL
2818
+ }, children);
2819
+ }
2820
+ //#endregion
2821
+ //#region src/gen/slide/notes.ts
2822
+ /**
2823
+ * Collect the speaker-notes runs for a slide (flattened across any number of `addNotes()` calls).
2824
+ * @param {PresSlideInternal} slide - the slide object
2825
+ * @return {TextProps[]} notes text runs in document order
2826
+ */
2827
+ function getNotesRuns(slide) {
2828
+ const runs = [];
2829
+ slide._slideObjects.forEach((obj) => {
2830
+ if (obj._type === "notes" && obj.text) runs.push(...obj.text);
2831
+ });
2832
+ return runs;
2833
+ }
2834
+ /**
2835
+ * Resolve a slide's notes runs against the notes part's own relationship ids.
2836
+ *
2837
+ * Notes rels use their own namespace, independent of `slide._rels` (which serialize to
2838
+ * `slideN.xml.rels`). The notes part always reserves rId1=notesMaster and rId2=slide, so
2839
+ * hyperlink rels are allocated starting at rId3.
2840
+ *
2841
+ * Pure. The ids go on copies of the runs, never on the caller's run or hyperlink objects: the same
2842
+ * hyperlink object can also sit on a slide run, whose id lives in the slide's namespace, and a deck
2843
+ * can be written twice or gain notes between writes. Each notes-part writer computes this once and
2844
+ * hands the result to both the body and the rels file, which is what keeps the two in agreement.
2845
+ *
2846
+ * Only external `url` hyperlinks are supported; any other target is left off the copy, and a
2847
+ * `slide` target says so.
2848
+ * @param {PresSlideInternal} slide - the slide object
2849
+ * @return {NotesSlideContent} the notes hyperlink relationships and the runs referencing them
2850
+ */
2851
+ function buildNotesSlideRels(slide) {
2852
+ const NOTES_REL_RESERVED = 2;
2853
+ const rels = [];
2854
+ let lastRid = NOTES_REL_RESERVED;
2855
+ return {
2856
+ rels,
2857
+ runs: getNotesRuns(slide).map((run) => {
2858
+ 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
+ }
2868
+ lastRid++;
2869
+ rels.push(hyperlinkRel(lastRid, hyperlink));
2870
+ const linked = {
2871
+ ...hyperlink,
2872
+ _rId: lastRid
2873
+ };
2874
+ return {
2875
+ ...run,
2876
+ options: {
2877
+ ...run.options,
2878
+ hyperlink: linked
2879
+ }
2880
+ };
2881
+ })
2882
+ };
2883
+ }
2884
+ /**
2885
+ * Build the `<p:txBody>` paragraphs for the notes placeholder.
2886
+ * Runs are split into `<a:p>` paragraphs on newlines; each run is serialized with the standard
2887
+ * text-run generator so inline formatting and `<a:hlinkClick>` markup are emitted consistently.
2888
+ * @param {TextProps[]} runs - the notes runs, as {@link buildNotesSlideRels} resolved them
2889
+ * @return {string} XML string of `<a:p>` paragraphs
2890
+ */
2891
+ function genXmlNotesParagraphs(runs) {
2892
+ const paragraphs = [[]];
2893
+ runs.forEach((run) => {
2894
+ String(run.text ?? "").split("\n").forEach((segment, idx) => {
2895
+ if (idx > 0) paragraphs.push([]);
2896
+ const text = segment.replace(/\r/g, "");
2897
+ if (text !== "") paragraphs[paragraphs.length - 1]?.push({
2898
+ text,
2899
+ options: run.options || {}
2900
+ });
2901
+ });
2902
+ });
2903
+ return paragraphs.map((runs) => el("a:p", null, [raw(runs.map((run) => genXmlTextRun(run)).join("")), raw(voidEl("a:endParaRPr", {
2904
+ lang: "en-US",
2905
+ dirty: 0
2906
+ }))])).join("");
2907
+ }
2908
+ /**
2909
+ * PowerPoint's creation-id extension, on the `p:cSld` of both notes parts.
2910
+ *
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
2913
+ * changes one of them. It is deliberately constant, which is what keeps the two notes parts
2914
+ * byte-identical across builds.
2915
+ */
2916
+ const NOTES_CREATION_ID = 1024086991;
2917
+ /** The `p:extLst` carrying {@link NOTES_CREATION_ID}, identical on the notes master and notes slide. */
2918
+ function notesExtLst() {
2919
+ return el("p:extLst", null, raw(el("p:ext", { uri: "{BB962C8B-B14F-4D97-AF65-F5344CB8AC3E}" }, raw(voidEl("p14:creationId", {
2920
+ "xmlns:p14": OOXML_NS.p14,
2921
+ val: NOTES_CREATION_ID
2922
+ })))));
2923
+ }
2924
+ /**
2925
+ * Generate XML for Notes Master (notesMaster1.xml)
2926
+ * @returns {string} XML
2927
+ */
2928
+ function makeXmlNotesMaster() {
2929
+ const xfrmOffExt = (x, y, cx, cy) => xfrmEl("a:xfrm", {
2930
+ x,
2931
+ y,
2932
+ cx,
2933
+ cy
2934
+ });
2935
+ const rectGeom = prstGeomRect();
2936
+ const spLocks = (attrs) => el("p:cNvSpPr", null, raw(voidEl("a:spLocks", attrs)));
2937
+ const nvPr = (phAttrs) => el("p:nvPr", null, raw(voidEl("p:ph", phAttrs)));
2938
+ const bodyPr = (attrs) => voidEl("a:bodyPr", attrs);
2939
+ const lstStyleLvl1 = (algn) => el("a:lstStyle", null, raw(el("a:lvl1pPr", { algn }, raw(voidEl("a:defRPr", { sz: 1200 })))));
2940
+ const emptyPara = el("a:p", null, raw(voidEl("a:endParaRPr", { lang: "en-US" })));
2941
+ const spTree = spTreeEl([
2942
+ el("p:sp", null, [
2943
+ raw(el("p:nvSpPr", null, [
2944
+ raw(voidEl("p:cNvPr", {
2945
+ id: 2,
2946
+ name: "Header Placeholder 1"
2947
+ })),
2948
+ raw(spLocks({ noGrp: 1 })),
2949
+ raw(nvPr({
2950
+ type: "hdr",
2951
+ sz: "quarter"
2952
+ }))
2953
+ ])),
2954
+ raw(el("p:spPr", null, [raw(xfrmOffExt(0, 0, 2971800, 458788)), raw(rectGeom)])),
2955
+ raw(el("p:txBody", null, [
2956
+ raw(bodyPr({
2957
+ vert: "horz",
2958
+ lIns: 91440,
2959
+ tIns: 45720,
2960
+ rIns: 91440,
2961
+ bIns: 45720,
2962
+ rtlCol: 0
2963
+ })),
2964
+ raw(lstStyleLvl1("l")),
2965
+ raw(emptyPara)
2966
+ ]))
2967
+ ]),
2968
+ el("p:sp", null, [
2969
+ raw(el("p:nvSpPr", null, [
2970
+ raw(voidEl("p:cNvPr", {
2971
+ id: 3,
2972
+ name: "Date Placeholder 2"
2973
+ })),
2974
+ raw(spLocks({ noGrp: 1 })),
2975
+ raw(nvPr({
2976
+ type: "dt",
2977
+ idx: 1
2978
+ }))
2979
+ ])),
2980
+ raw(el("p:spPr", null, [raw(xfrmOffExt(3884613, 0, 2971800, 458788)), raw(rectGeom)])),
2981
+ raw(el("p:txBody", null, [
2982
+ raw(bodyPr({
2983
+ vert: "horz",
2984
+ lIns: 91440,
2985
+ tIns: 45720,
2986
+ rIns: 91440,
2987
+ bIns: 45720,
2988
+ rtlCol: 0
2989
+ })),
2990
+ raw(lstStyleLvl1("r")),
2991
+ raw(el("a:p", null, [raw(el("a:fld", {
2992
+ id: "{5282F153-3F37-0F45-9E97-73ACFA13230C}",
2993
+ type: "datetimeFigureOut"
2994
+ }, [raw(voidEl("a:rPr", { lang: "en-US" })), raw(el("a:t", null, "7/23/19"))])), raw(voidEl("a:endParaRPr", { lang: "en-US" }))]))
2995
+ ]))
2996
+ ]),
2997
+ el("p:sp", null, [
2998
+ raw(el("p:nvSpPr", null, [
2999
+ raw(voidEl("p:cNvPr", {
3000
+ id: 4,
3001
+ name: "Slide Image Placeholder 3"
3002
+ })),
3003
+ raw(spLocks({
3004
+ noGrp: 1,
3005
+ noRot: 1,
3006
+ noChangeAspect: 1
3007
+ })),
3008
+ raw(nvPr({
3009
+ type: "sldImg",
3010
+ idx: 2
3011
+ }))
3012
+ ])),
3013
+ raw(el("p:spPr", null, [
3014
+ raw(xfrmOffExt(685800, 1143e3, 5486400, 3086100)),
3015
+ raw(rectGeom),
3016
+ raw(voidEl("a:noFill")),
3017
+ raw(el("a:ln", { w: 12700 }, raw(el("a:solidFill", null, raw(voidEl("a:prstClr", { val: "black" }))))))
3018
+ ])),
3019
+ raw(el("p:txBody", null, [
3020
+ raw(bodyPr({
3021
+ vert: "horz",
3022
+ lIns: 91440,
3023
+ tIns: 45720,
3024
+ rIns: 91440,
3025
+ bIns: 45720,
3026
+ rtlCol: 0,
3027
+ anchor: "ctr"
3028
+ })),
3029
+ raw(voidEl("a:lstStyle")),
3030
+ raw(emptyPara)
3031
+ ]))
3032
+ ]),
3033
+ el("p:sp", null, [
3034
+ raw(el("p:nvSpPr", null, [
3035
+ raw(voidEl("p:cNvPr", {
3036
+ id: 5,
3037
+ name: "Notes Placeholder 4"
3038
+ })),
3039
+ raw(spLocks({ noGrp: 1 })),
3040
+ raw(nvPr({
3041
+ type: "body",
3042
+ sz: "quarter",
3043
+ idx: 3
3044
+ }))
3045
+ ])),
3046
+ raw(el("p:spPr", null, [raw(xfrmOffExt(685800, 4400550, 5486400, 3600450)), raw(rectGeom)])),
3047
+ raw(el("p:txBody", null, [
3048
+ raw(bodyPr({
3049
+ vert: "horz",
3050
+ lIns: 91440,
3051
+ tIns: 45720,
3052
+ rIns: 91440,
3053
+ bIns: 45720,
3054
+ rtlCol: 0
3055
+ })),
3056
+ raw(voidEl("a:lstStyle")),
3057
+ raw([
3058
+ "Click to edit Master text styles",
3059
+ "Second level",
3060
+ "Third level",
3061
+ "Fourth level",
3062
+ "Fifth level"
3063
+ ].map((text, lvl) => el("a:p", null, [raw(voidEl("a:pPr", { lvl })), raw(el("a:r", null, [raw(voidEl("a:rPr", { lang: "en-US" })), raw(el("a:t", null, text))]))])).join(""))
3064
+ ]))
3065
+ ]),
3066
+ el("p:sp", null, [
3067
+ raw(el("p:nvSpPr", null, [
3068
+ raw(voidEl("p:cNvPr", {
3069
+ id: 6,
3070
+ name: "Footer Placeholder 5"
3071
+ })),
3072
+ raw(spLocks({ noGrp: 1 })),
3073
+ raw(nvPr({
3074
+ type: "ftr",
3075
+ sz: "quarter",
3076
+ idx: 4
3077
+ }))
3078
+ ])),
3079
+ raw(el("p:spPr", null, [raw(xfrmOffExt(0, 8685213, 2971800, 458787)), raw(rectGeom)])),
3080
+ raw(el("p:txBody", null, [
3081
+ raw(bodyPr({
3082
+ vert: "horz",
3083
+ lIns: 91440,
3084
+ tIns: 45720,
3085
+ rIns: 91440,
3086
+ bIns: 45720,
3087
+ rtlCol: 0,
3088
+ anchor: "b"
3089
+ })),
3090
+ raw(lstStyleLvl1("l")),
3091
+ raw(emptyPara)
3092
+ ]))
3093
+ ]),
3094
+ el("p:sp", null, [
3095
+ raw(el("p:nvSpPr", null, [
3096
+ raw(voidEl("p:cNvPr", {
3097
+ id: 7,
3098
+ name: "Slide Number Placeholder 6"
3099
+ })),
3100
+ raw(spLocks({ noGrp: 1 })),
3101
+ raw(nvPr({
3102
+ type: "sldNum",
3103
+ sz: "quarter",
3104
+ idx: 5
3105
+ }))
3106
+ ])),
3107
+ raw(el("p:spPr", null, [raw(xfrmOffExt(3884613, 8685213, 2971800, 458787)), raw(rectGeom)])),
3108
+ raw(el("p:txBody", null, [
3109
+ raw(bodyPr({
3110
+ vert: "horz",
3111
+ lIns: 91440,
3112
+ tIns: 45720,
3113
+ rIns: 91440,
3114
+ bIns: 45720,
3115
+ rtlCol: 0,
3116
+ anchor: "b"
3117
+ })),
3118
+ raw(lstStyleLvl1("r")),
3119
+ raw(el("a:p", null, [raw(el("a:fld", {
3120
+ id: "{CE5E9CC1-C706-0F49-92D6-E571CC5EEA8F}",
3121
+ type: "slidenum"
3122
+ }, [raw(voidEl("a:rPr", { lang: "en-US" })), raw(el("a:t", null, "‹#›"))])), raw(voidEl("a:endParaRPr", { lang: "en-US" }))]))
3123
+ ]))
3124
+ ])
3125
+ ]);
3126
+ const extLst = notesExtLst();
3127
+ const cSld = el("p:cSld", null, [
3128
+ raw(el("p:bg", null, raw(el("p:bgRef", { idx: 1001 }, raw(voidEl("a:schemeClr", { val: "bg1" })))))),
3129
+ raw(spTree),
3130
+ raw(extLst)
3131
+ ]);
3132
+ const clrMap = voidEl("p:clrMap", DEFAULT_COLOR_MAP);
3133
+ const notesStyleLevel = (n, marL) => lvlPPr(n, {
3134
+ marL,
3135
+ algn: "l"
3136
+ }, [raw(themeFontDefRPr("mn", {
3137
+ sz: 1200,
3138
+ kern: 1200
3139
+ }))]);
3140
+ const notesStyle = el("p:notesStyle", null, raw(LEVEL_MARGINS_EMU.map((marL, i) => notesStyleLevel(i + 1, marL)).join("")));
3141
+ return XML_DECL + "\r\n" + el("p:notesMaster", { ...PML_ROOT_NS }, [
3142
+ raw(cSld),
3143
+ raw(clrMap),
3144
+ raw(notesStyle)
3145
+ ]);
3146
+ }
3147
+ /**
3148
+ * Creates Notes Slide (`ppt/notesSlides/notesSlide1.xml`)
3149
+ * @param {PresSlideInternal} slide - the slide object to transform into XML
3150
+ * @param {NotesSlideContent} notes - the slide's notes, from {@link buildNotesSlideRels}; the same
3151
+ * value has to reach {@link makeXmlNotesSlideRel}
3152
+ * @return {string} XML
3153
+ */
3154
+ function makeXmlNotesSlide(slide, notes) {
3155
+ return makeXmlNotesSlideSkeleton(genXmlNotesParagraphs(notes.runs), slide._slideNum);
3156
+ }
3157
+ /**
3158
+ * The notes slide's fixed frame, with the only two slide-dependent strings passed in.
3159
+ *
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
3162
+ * paragraphs and the cached slide number is a constant. It is a parameter of its own
3163
+ * because the read model authors notes onto a *loaded* deck (`Slide.addNotes`), where
3164
+ * there is no `PresSlideInternal` to serialize but the part must come out identical:
3165
+ * a second private copy of this frame is exactly the drift `src/ooxml/` exists to
3166
+ * prevent (see docs/contributing/architecture.md).
3167
+ * @param {string} bodyParagraphsXml - `a:p` children of the body placeholder's `p:txBody`
3168
+ * @param {number} slideNum - cached value of the slide-number `a:fld`
3169
+ * @return {string} XML
3170
+ */
3171
+ function makeXmlNotesSlideSkeleton(bodyParagraphsXml, slideNum) {
3172
+ const spTree = spTreeEl([
3173
+ el("p:sp", null, [raw(el("p:nvSpPr", null, [
3174
+ raw(voidEl("p:cNvPr", {
3175
+ id: 2,
3176
+ name: "Slide Image Placeholder 1"
3177
+ })),
3178
+ raw(el("p:cNvSpPr", null, raw(voidEl("a:spLocks", {
3179
+ noGrp: 1,
3180
+ noRot: 1,
3181
+ noChangeAspect: 1
3182
+ })))),
3183
+ raw(el("p:nvPr", null, raw(voidEl("p:ph", { type: "sldImg" }))))
3184
+ ])), raw(voidEl("p:spPr"))]),
3185
+ el("p:sp", null, [
3186
+ raw(el("p:nvSpPr", null, [
3187
+ raw(voidEl("p:cNvPr", {
3188
+ id: 3,
3189
+ name: "Notes Placeholder 2"
3190
+ })),
3191
+ raw(el("p:cNvSpPr", null, raw(voidEl("a:spLocks", { noGrp: 1 })))),
3192
+ raw(el("p:nvPr", null, raw(voidEl("p:ph", {
3193
+ type: "body",
3194
+ idx: 1
3195
+ }))))
3196
+ ])),
3197
+ raw(voidEl("p:spPr")),
3198
+ raw(el("p:txBody", null, [
3199
+ raw(voidEl("a:bodyPr")),
3200
+ raw(voidEl("a:lstStyle")),
3201
+ raw(bodyParagraphsXml)
3202
+ ]))
3203
+ ]),
3204
+ el("p:sp", null, [
3205
+ raw(el("p:nvSpPr", null, [
3206
+ raw(voidEl("p:cNvPr", {
3207
+ id: 4,
3208
+ name: "Slide Number Placeholder 3"
3209
+ })),
3210
+ raw(el("p:cNvSpPr", null, raw(voidEl("a:spLocks", { noGrp: 1 })))),
3211
+ raw(el("p:nvPr", null, raw(voidEl("p:ph", {
3212
+ type: "sldNum",
3213
+ sz: "quarter",
3214
+ idx: 10
3215
+ }))))
3216
+ ])),
3217
+ raw(voidEl("p:spPr")),
3218
+ raw(el("p:txBody", null, [
3219
+ raw(voidEl("a:bodyPr")),
3220
+ raw(voidEl("a:lstStyle")),
3221
+ raw(el("a:p", null, [raw(el("a:fld", {
3222
+ id: SLDNUMFLDID,
3223
+ type: "slidenum"
3224
+ }, [raw(voidEl("a:rPr", { lang: "en-US" })), raw(el("a:t", null, slideNum))])), raw(voidEl("a:endParaRPr", { lang: "en-US" }))]))
3225
+ ]))
3226
+ ])
3227
+ ]);
3228
+ const extLst = notesExtLst();
3229
+ const cSld = el("p:cSld", null, [raw(spTree), raw(extLst)]);
3230
+ return XML_DECL + "\r\n" + el("p:notes", { ...PML_ROOT_NS }, [raw(cSld), raw(el("p:clrMapOvr", null, raw(voidEl("a:masterClrMapping"))))]);
3231
+ }
3232
+ /**
3233
+ * Generates XML string for a notes-slide relation file (`ppt/notesSlides/_rels/notesSlideN.xml.rels`).
3234
+ * rId1=notesMaster and rId2=slide are always reserved; any notes hyperlink rels follow (rId3+).
3235
+ * @param {NotesSlideContent} notes - the slide's notes, the value {@link makeXmlNotesSlide} was given
3236
+ * @param {number} slideNumber - 1-indexed slide number the notes part belongs to
3237
+ * @return {string} XML
3238
+ */
3239
+ function makeXmlNotesSlideRel(notes, slideNumber) {
3240
+ const hlinkRels = notes.rels.map((rel) => externalHyperlinkRel(rel.rId, rel.Target)).join("");
3241
+ return XML_DECL + relationshipsEl([
3242
+ relationshipEl(1, NOTES_MASTER_REL, targetFromPptSubpart(NOTES_MASTER_PATH)),
3243
+ relationshipEl(2, SLIDE_REL, targetFromPptSubpart(slidePath(slideNumber))),
3244
+ hlinkRels
3245
+ ], {
3246
+ openPrefix: "\n ",
3247
+ childPrefix: "\n ",
3248
+ closePrefix: ""
3249
+ });
3250
+ }
3251
+ /**
3252
+ * Creates `ppt/notesMasters/_rels/notesMaster1.xml.rels`
3253
+ * @return {string} XML
3254
+ */
3255
+ function makeXmlNotesMasterRel() {
3256
+ return relationshipsPart([relationshipEl(1, THEME_REL, "../theme/theme2.xml")], {
3257
+ childPrefix: "\n ",
3258
+ closePrefix: "\n "
3259
+ });
3260
+ }
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 };
3263
+
3264
+ //# sourceMappingURL=notes-Dh12JAbn.js.map