pptx-ts 3.7.0 → 4.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +116 -269
- package/dist/autopage-odPz7zZO.js +536 -0
- package/dist/autopage-odPz7zZO.js.map +1 -0
- package/dist/binary-string-CKVAYI3M.js +24 -0
- package/dist/binary-string-CKVAYI3M.js.map +1 -0
- package/dist/body-insets-Dw_BeWD_.js +54 -0
- package/dist/body-insets-Dw_BeWD_.js.map +1 -0
- package/dist/browser.d.ts +29 -9
- package/dist/browser.d.ts.map +1 -1
- package/dist/browser.js +41 -14
- package/dist/browser.js.map +1 -1
- package/dist/clip-Bl01BPcN.d.ts +67 -0
- package/dist/clip-Bl01BPcN.d.ts.map +1 -0
- package/dist/diagnostics-D8ot4Ei7.d.ts +244 -0
- package/dist/diagnostics-D8ot4Ei7.d.ts.map +1 -0
- package/dist/embedded-fonts-DarT9aoa.d.ts +20 -0
- package/dist/embedded-fonts-DarT9aoa.d.ts.map +1 -0
- package/dist/entry-compose-DCnvBZz2.d.ts +1046 -0
- package/dist/entry-compose-DCnvBZz2.d.ts.map +1 -0
- package/dist/{enums-CvRYniUA.d.ts → enums-BUKTUB6V.d.ts} +21 -13
- package/dist/{enums-CvRYniUA.d.ts.map → enums-BUKTUB6V.d.ts.map} +1 -1
- package/dist/{errors-Bvln0H-7.js → errors-C4oENPcu.js} +92 -2
- package/dist/errors-C4oENPcu.js.map +1 -0
- package/dist/families.d.ts +260 -0
- package/dist/families.d.ts.map +1 -0
- package/dist/families.js +57 -0
- package/dist/families.js.map +1 -0
- package/dist/{fit-BKs0zxMI.js → fit-Cb7naFj6.js} +249 -167
- package/dist/fit-Cb7naFj6.js.map +1 -0
- package/dist/font-metrics-v5fn7s7e.d.ts +613 -0
- package/dist/font-metrics-v5fn7s7e.d.ts.map +1 -0
- package/dist/grid-DikTjFey.js +187 -0
- package/dist/grid-DikTjFey.js.map +1 -0
- package/dist/{html-dom-B0I0QSbU.js → html-dom-Iuog2wdK.js} +121 -91
- package/dist/html-dom-Iuog2wdK.js.map +1 -0
- package/dist/html.d.ts +6 -6
- package/dist/html.d.ts.map +1 -1
- package/dist/html.js +7 -6
- package/dist/html.js.map +1 -1
- package/dist/image-size-CvN2iCA4.js +1611 -0
- package/dist/image-size-CvN2iCA4.js.map +1 -0
- package/dist/index.d.ts +24 -10
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +31 -11
- package/dist/index.js.map +1 -1
- package/dist/inspect.d.ts +40 -31
- package/dist/inspect.d.ts.map +1 -1
- package/dist/inspect.js +40 -48
- package/dist/inspect.js.map +1 -1
- package/dist/math.d.ts +7 -5
- package/dist/math.d.ts.map +1 -1
- package/dist/math.js +16 -7
- package/dist/math.js.map +1 -1
- package/dist/measure.d.ts +12 -225
- package/dist/measure.d.ts.map +1 -1
- package/dist/measure.js +3 -3
- package/dist/node.d.ts +22 -8
- package/dist/node.d.ts.map +1 -1
- package/dist/node.js +27 -7
- package/dist/node.js.map +1 -1
- package/dist/notes-Dh12JAbn.js +3264 -0
- package/dist/notes-Dh12JAbn.js.map +1 -0
- package/dist/options-internal-WqgMf720.js +43 -0
- package/dist/options-internal-WqgMf720.js.map +1 -0
- package/dist/{package-CaOLXSNi.d.ts → package-J49Ck6S2.d.ts} +27 -12
- package/dist/package-J49Ck6S2.d.ts.map +1 -0
- package/dist/playbtn-CUqQdoDf.js +22 -0
- package/dist/playbtn-CUqQdoDf.js.map +1 -0
- package/dist/{slide-ZqAdVvHV.d.ts → pres-C6qgOFAa.d.ts} +1021 -1423
- package/dist/pres-C6qgOFAa.d.ts.map +1 -0
- package/dist/{presentation-DrJtj3-C.js → presentation-DGYlAWhl.js} +3208 -2727
- package/dist/presentation-DGYlAWhl.js.map +1 -0
- package/dist/{presentation-types-CDjScsev.d.ts → presentation-types-8jLQHI4M.d.ts} +1070 -683
- package/dist/presentation-types-8jLQHI4M.d.ts.map +1 -0
- package/dist/read.d.ts +9 -9
- package/dist/read.d.ts.map +1 -1
- package/dist/read.js +4 -4
- package/dist/script.d.ts +80 -66
- package/dist/script.d.ts.map +1 -1
- package/dist/script.js +2399 -1139
- package/dist/script.js.map +1 -1
- package/dist/{shapes-BemroU-z.js → shapes-re0KyQk_.js} +2807 -1609
- package/dist/shapes-re0KyQk_.js.map +1 -0
- package/dist/slide-Dx2ZeVZ6.d.ts +1256 -0
- package/dist/slide-Dx2ZeVZ6.d.ts.map +1 -0
- package/dist/slide-size-BalTjFuv.js +88 -0
- package/dist/slide-size-BalTjFuv.js.map +1 -0
- package/dist/{units-internal-Dg95w_YD.js → table-cell-margins-EVg0EwxT.js} +235 -273
- package/dist/table-cell-margins-EVg0EwxT.js.map +1 -0
- package/dist/table-dom-BTDP3lEP.js +21 -0
- package/dist/table-dom-BTDP3lEP.js.map +1 -0
- package/dist/text-anchor-CrYXAfUu.js +176 -0
- package/dist/text-anchor-CrYXAfUu.js.map +1 -0
- package/dist/text-sN64Otf_.js +3980 -0
- package/dist/text-sN64Otf_.js.map +1 -0
- package/dist/transition-speed-kUEWDWTx.js +14 -0
- package/dist/transition-speed-kUEWDWTx.js.map +1 -0
- package/dist/units-internal-XJlf-hOl.js +649 -0
- package/dist/units-internal-XJlf-hOl.js.map +1 -0
- package/dist/zip-C0leZZMj.d.ts +67 -0
- package/dist/zip-C0leZZMj.d.ts.map +1 -0
- package/dist/zip-CKd8ihcf.js +177 -0
- package/dist/zip-CKd8ihcf.js.map +1 -0
- package/dist/zip.d.ts +3 -68
- package/dist/zip.js +3 -163
- package/dist/zoom-DPUHh85d.js +8865 -0
- package/dist/zoom-DPUHh85d.js.map +1 -0
- package/package.json +106 -19
- package/skills/ts-pptx-upstream/SKILL.md +58 -57
- package/dist/autopage-DkjYZUFD.js +0 -423
- package/dist/autopage-DkjYZUFD.js.map +0 -1
- package/dist/diagnostics-DfBdDHre.d.ts +0 -829
- package/dist/diagnostics-DfBdDHre.d.ts.map +0 -1
- package/dist/errors-Bvln0H-7.js.map +0 -1
- package/dist/errors-PflMPWBn.d.ts +0 -182
- package/dist/errors-PflMPWBn.d.ts.map +0 -1
- package/dist/fit-BKs0zxMI.js.map +0 -1
- package/dist/html-dom-B0I0QSbU.js.map +0 -1
- package/dist/namespaces-BVnNu7sg.js +0 -666
- package/dist/namespaces-BVnNu7sg.js.map +0 -1
- package/dist/notes-CJFJaG6M.js +0 -1724
- package/dist/notes-CJFJaG6M.js.map +0 -1
- package/dist/package-CaOLXSNi.d.ts.map +0 -1
- package/dist/pres-BcBR3CSM.d.ts +0 -202
- package/dist/pres-BcBR3CSM.d.ts.map +0 -1
- package/dist/presentation-DrJtj3-C.js.map +0 -1
- package/dist/presentation-types-CDjScsev.d.ts.map +0 -1
- package/dist/shapes-BemroU-z.js.map +0 -1
- package/dist/slide-ZqAdVvHV.d.ts.map +0 -1
- package/dist/text-B8SLEaZx.js +0 -11327
- package/dist/text-B8SLEaZx.js.map +0 -1
- package/dist/units-1kRzYShk.js +0 -231
- package/dist/units-1kRzYShk.js.map +0 -1
- package/dist/units-internal-Dg95w_YD.js.map +0 -1
- package/dist/zip.d.ts.map +0 -1
- 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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
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, "	").replace(/\n/g, " ").replace(/\r/g, " ");
|
|
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 `	`/` `/` ` 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
|
+
* (`•`, 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 = "•";
|
|
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, "•", "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, "•", "bullet");
|
|
1782
|
+
} else if (opts.bullet === "inherit") {} else if (opts.bullet) {
|
|
1783
|
+
defaultMarL = bulletHangMarL(bulletMarL, indentLevel);
|
|
1784
|
+
defaultIndent = -bulletMarL;
|
|
1785
|
+
strXmlBullet = buCharEl(void 0, "•", "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;` 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
|