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