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