pptx-kit 0.0.0 → 0.3.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/CHANGELOG.md ADDED
@@ -0,0 +1,1657 @@
1
+ # pptx-kit
2
+
3
+ ## 0.3.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 3459aa5: `createPresentation()` now returns an immediately-authorable deck
8
+
9
+ Previously `createPresentation()` returned an OPC package with only the OPC
10
+ defaults — no slide master, layouts, theme, or slide size — so
11
+ `getSlideLayouts()` came back empty and `addSlide({ layout })` was
12
+ impossible. From-scratch authoring (a headline feature in the README) did
13
+ not actually work without loading a template file.
14
+
15
+ `createPresentation()` now ships a slide master, the Office theme, and three
16
+ layouts — `Blank`, `Title Slide`, and `Title and Content` — so you can go
17
+ straight to `addSlide` / `addTitleSlide` / `addContentSlide` and `savePresentation`.
18
+ Every emitted part is validated against the ECMA-376 XSDs in CI. The slide
19
+ size defaults to 16:9 and is selectable: `createPresentation({ size: '4:3' })`.
20
+
21
+ Also in this release (input-validation hardening at the authoring boundary):
22
+
23
+ - `addSlideChart` now rejects a series `color` (and `pointColors` /
24
+ `trendline.color` / plot- and chart-area fills / axis & gridline colors)
25
+ that isn't an sRGB hex (`#RRGGBB` or `RRGGBB`) with a clear error, instead
26
+ of silently emitting an invalid `<a:srgbClr val="…"/>` that PowerPoint
27
+ dropped or repaired. Bare `RRGGBB` (no `#`) is accepted and normalized;
28
+ scheme tokens like `accent1` are correctly rejected, since charts emit
29
+ `srgbClr`.
30
+ - `addSlideTable` with empty `rows: []` (or a row with no cells) now throws
31
+ an actionable `addSlideTable: …` error at the boundary rather than
32
+ producing a grid-less `<a:tbl>` that triggers PowerPoint's repair dialog.
33
+ (The error message previously named the old internal `addTable` path.)
34
+ - `findSlideLayout`'s case-sensitive, locale-dependent name matching is now
35
+ documented in its JSDoc and the README, pointing readers to the
36
+ locale-stable `findSlideLayoutByType` and to `RegExp`/`i` for
37
+ case-insensitive name lookups. No behavior change.
38
+
39
+ No breaking changes. `createPresentation()` keeps its zero-argument call
40
+ signature; the new `{ size }` options object is optional.
41
+
42
+ ## 0.2.0
43
+
44
+ ### Minor Changes
45
+
46
+ - b03e0cb: feat: fidelity calibration sweep — measured against LibreOffice ground truth,
47
+ mean fg-SSIM rose from ≈0.66 to ≈0.78 (≈0.81 excluding documented
48
+ divergences). Body placeholders now inherit the master `bodyStyle` bullet and
49
+ hanging indent through the paragraph cascade (new `bullet` field on
50
+ `ParagraphProperties` from `getParagraphPropertiesEffective`); charts no
51
+ longer invent a legend when the XML authors no `<c:legend>`, and the value
52
+ axis gets Excel-style headroom above the data max with the tick step
53
+ preserved; the chart builder writes `<c:smooth val="0"/>` explicitly on line
54
+ series (the schema default for an absent element is smooth=1, which made
55
+ LibreOffice draw unauthored lines as curves); and the pure-SVG text layer is
56
+ nudged 0.75px left to land on LibreOffice's pixel grid.
57
+
58
+ ## 0.1.0
59
+
60
+ ### Minor Changes
61
+
62
+ - 263bf52: feat: `getShapeAdjustValues(shape)` returns the `<a:prstGeom><a:avLst>
63
+ <a:gd name=… fmla="val N"/></a:avLst>` map (preset adjust-handle
64
+ values). Only literal `val` formulas are surfaced; computed formulas
65
+ (`pin`, `+-`, etc.) reference the preset's built-in guides and
66
+ aren't useful without them.
67
+
68
+ Playground reads `adj` on `roundRect` to project the authored corner
69
+ radius — previously every rounded rectangle painted at a hard-coded
70
+ 18%. Other presets (callouts, arrows, etc.) can adopt the same getter
71
+ as their renderers grow.
72
+
73
+ - a944352: feat(site/playground): render auto-numbered bullets. Paragraphs with
74
+ `bulletStyle === 'number'` or `{ autoNum: '…' }` now emit the next
75
+ number in sequence (1., 2., 3., …; A., B., C., …; i., ii., iii., …)
76
+ rather than a generic dot. Counter resets on a non-numbered paragraph
77
+ or a level change, matching PowerPoint's behaviour.
78
+
79
+ Covers the common `ST_TextAutoNumberScheme` tokens — arabicPeriod /
80
+ ParenR / ParenBoth, romanUc / Lc with Period / ParenR / ParenBoth,
81
+ alphaUc / Lc with Period / ParenR / ParenBoth.
82
+
83
+ - 63c4453: feat: chart axis _tick labels_ honor authored `<c:txPr>` font / color.
84
+ `ChartSpec.categoryAxisLabelStyle` and `valueAxisLabelStyle` carry the
85
+ font / color extracted from `<c:catAx><c:txPr>` and `<c:valAx><c:txPr>`.
86
+ A shared `axisTickAttrs` helper composes the SVG `font-*` / `fill`
87
+ attributes; the value-axis renderer and category-axis renderer both
88
+ project it onto every tick label.
89
+ - e60dc26: feat: chart value-axis tick marks. `ChartSpec.valueAxisMajorTickMark`
90
+ and `categoryAxisMajorTickMark` carry `<c:majorTickMark val="in|out|
91
+ cross|none"/>`. The playground value-axis renderer draws short stubs
92
+ on the appropriate side of the plot edge (default `out` matches
93
+ PowerPoint's stock look); `none` suppresses them entirely.
94
+ - 3a2b974: feat: chart axis titles honor authored `<a:rPr>` font / color.
95
+ `ChartSpec.categoryAxisTitleStyle` and `valueAxisTitleStyle` carry the
96
+ same `ChartTextStyle` shape as `titleStyle`. The playground renderer
97
+ projects size / bold / italic / fill onto both axis title labels,
98
+ sharing the helper that drives the chart title.
99
+ - b8c676d: feat(site/playground): bar chart category-axis labels honor
100
+ `categoryAxisLabelRotationDeg`. The horizontal-value renderer used
101
+ the rotation field only for column charts (categories along the
102
+ x-axis); now the bar variant (categories down the y-axis) also
103
+ rotates each label around its anchor and widens its ellipsis budget
104
+ for tilted labels.
105
+ - 0b61a96: feat(site/playground): apply the 3-level gradient bg cascade. When
106
+ the slide reports `'gradient'` but doesn't author the actual stops,
107
+ the renderer walks slide → layout → master gradient-fill readers
108
+ to find the inherited gradient definition.
109
+ - 2896447: feat: `getSlideLayoutBackgroundImageBytes(pres, layout)` and
110
+ `getSlideMasterBackgroundImageBytes(pres, layout)` complete the
111
+ picture-background cascade. The slide reader already returned bytes
112
+ for slide-level `<a:blipFill>` backgrounds; the new readers resolve
113
+ the same shape on layouts and masters via their own rel lists. The
114
+ playground renderer threads slide → layout → master fallback, so
115
+ template-defined photo backgrounds finally show on inheriting slides.
116
+ - 8a4dafb: feat(site/playground): apply the 3-level pattern bg cascade. When the
117
+ slide reports `'pattern'` but doesn't author the actual pattern preset,
118
+ the renderer now walks slide → layout → master pattern-fill readers,
119
+ paralleling the gradient cascade.
120
+ - 4b1fd76: feat: `getSlideLayoutBackgroundPatternFill(pres, layout)` and
121
+ `getSlideMasterBackgroundPatternFill(pres, layout)` complete the
122
+ pattern-background cascade. Slides reporting `'pattern'` can now
123
+ resolve the actual preset / colors by walking slide → layout →
124
+ master, paralleling the gradient / solid / blip cascades.
125
+ - 63dee61: feat: `getShapeBodyPrEffective(pres, shape)` — `<a:bodyPr>` cascade
126
+ covering anchor, wrap, vertical-text direction, and inset margins.
127
+ Walks shape → layout placeholder → master placeholder bodyPr the same
128
+ way the rPr / pPr cascades do. Playground uses it so placeholders
129
+ inherit text alignment / margins from the layout / master without
130
+ each slide having to re-author them.
131
+ - 36bd14d: feat(site/playground): shape text honors `<a:bodyPr wrap="none"/>`.
132
+ The reader's effective wrap value was already threaded through; the
133
+ renderer now emits `white-space:nowrap` when wrap is `'none'`,
134
+ keeping single-line text frames (vertical labels, breadcrumbs,
135
+ fixed-width badges) from wrapping into multiple lines.
136
+ - e31b414: feat(site/playground): paragraphs with image bullets (`<a:pPr><a:buBlip>`)
137
+ render a filled-square glyph (■) instead of inheriting the default
138
+ round bullet. The reader already exposed `isParagraphBulletPicture`;
139
+ the playground now threads it through paragraph metadata so the
140
+ visual cue lands.
141
+ - e30c9f3: feat: `isParagraphBulletPicture(shape, p)` returns `true` when the
142
+ paragraph uses an image as its bullet (`<a:pPr><a:buBlip>`).
143
+ Renderers without image-bullet support can fall back to a generic
144
+ glyph; UIs that want to indicate the bullet is custom have a clean
145
+ yes/no signal.
146
+ - 263bf52: feat: `getParagraphBulletStyle(pres, shape, p)` returns the
147
+ paragraph-level bullet overrides — color (theme-resolved), percent
148
+ size, fixed-point size, font face — from `<a:buClr>` / `<a:buSzPct>`
149
+ / `<a:buSzPts>` / `<a:buFont>`. Playground projects each onto the
150
+ bullet `<span>`, so decks that style bullets in an accent color or
151
+ sized-up font (a common branding move) render correctly instead of
152
+ falling back to the body's color.
153
+ - b53b420: feat: chart category-axis tick labels honor `<a:bodyPr rot="N"/>`.
154
+ `ChartSpec.categoryAxisLabelRotationDeg` carries the authored rotation
155
+ (converted from OOXML's 60000ths-of-a-degree to plain degrees). The
156
+ playground renderer rotates each tick label around its anchor and
157
+ shifts the text-anchor side based on the sign of the rotation so dense
158
+ charts with 45°/-45°/90° rotated labels render the way PowerPoint
159
+ shows them. Rotated labels also get a longer truncation budget before
160
+ ellipsization.
161
+ - 4f5cc4c: feat: round out gridline color round-trip with 3 more fields —
162
+ `valueAxisMinorGridlineColor`, `categoryAxisMajorGridlineColor`, and
163
+ `categoryAxisMinorGridlineColor`. Previously only the value-axis major
164
+ color was carried. All four now share a new chart-builder
165
+ `gridlinesElement(local, color?)` helper and a chart-reader
166
+ `readGridlineColor(gl)` helper; the existing major-gridline color
167
+ inline parse was replaced with a call to the shared reader for
168
+ consistency.
169
+ - 9fba547: feat(chart): `ChartSpec.chartAreaFill` and `plotAreaFill` read
170
+ `<c:chartSpace><c:spPr><a:solidFill>` and `<c:plotArea><c:spPr>
171
+ <a:solidFill>`. Playground paints the chart-area backdrop in the
172
+ authored color (replacing the hard-coded white) and adds a tinted
173
+ rect behind the plot area when `plotAreaFill` is authored. Common
174
+ on branded dashboards that paint a subtle background behind the
175
+ chart bars.
176
+ - 9d79d53: feat: chart-area / plot-area authored outline strokes.
177
+ `ChartSpec.chartAreaStrokeColor` reads `<c:chartSpace><c:spPr><a:ln>`;
178
+ `ChartSpec.plotAreaStrokeColor` reads `<c:plotArea><c:spPr><a:ln>`.
179
+ The playground renderer projects them onto the chart-area card
180
+ border and the plot-area inner rect — branded charts with thick / no
181
+ / colored card borders finally render the way PowerPoint shows them.
182
+ - eb4159e: feat(chart): `ChartSpec.valueAxisHidden` and `categoryAxisHidden`
183
+ read `<c:valAx><c:delete val="1"/>` and `<c:catAx><c:delete val="1"/>`.
184
+ Playground skips rendering the axis when hidden — common on KPI tile
185
+ charts that show just the data points without axis labels.
186
+ - 2710f56: feat: `ChartSpec.categoryAxisLineColor` and `valueAxisLineColor` —
187
+ authored stroke color on the axis line itself
188
+ (`<c:catAx|valAx><c:spPr><a:ln><a:solidFill><a:srgbClr val=…/>`).
189
+ `undefined` falls back to the renderer's default. Read by chart-reader,
190
+ written by chart-builder in the correct CT_CatAx / CT_ValAx schema
191
+ order (after the tick-mark elements, before `<c:txPr>`).
192
+ - c5761f4: feat(chart): `ChartSpec.categoryAxisOrientation` and
193
+ `valueAxisOrientation` read `<c:catAx>/<c:valAx><c:scaling>
194
+ <c:orientation val="minMax|maxMin"/>`. Tools and renderers that
195
+ care about category render order (typically bar charts emit
196
+ `maxMin` so the first category sits at the top) can act on these
197
+ without dropping to XML.
198
+ - 45e889d: feat: `ChartSpec.valueAxis` reports the authored
199
+ `<c:valAx><c:scaling>` min / max. Playground respects them when
200
+ computing axis ranges, so charts with a fixed authored scale (e.g.
201
+ percentage charts pinned to 0..100) render with the same scale the
202
+ deck author saw instead of auto-fitting to the data.
203
+
204
+ Adds the `ChartAxisScaling` interface to the public type surface.
205
+
206
+ - 6dd7281: feat: `ChartSpec.categoryAxisTitleRotationDeg` and
207
+ `ChartSpec.valueAxisTitleRotationDeg` — rotation in plain degrees
208
+ (clockwise) on the per-axis title. Maps to
209
+ `<c:catAx|valAx><c:title><c:tx><c:rich><a:bodyPr rot="N"/>` (60000ths
210
+ of a degree on the wire). PowerPoint often emits `-90` on the value-
211
+ axis title; the field now survives round-trip. Read by chart-reader
212
+ via a new `readTitleRotationDeg` helper; written by chart-builder
213
+ through an extended `titleElement(title, style?, rotationDeg?)`
214
+ signature.
215
+ - 2bf32ca: feat(chart): `ChartSpec.categoryAxisTitle` and `valueAxisTitle` read
216
+ the per-axis `<c:title>` rich text on `<c:catAx>` (or `<c:dateAx>` /
217
+ `<c:serAx>`) and `<c:valAx>`. Playground paints the value-axis title
218
+ rotated -90° along the y-axis and the category-axis title centered
219
+ below the x-axis.
220
+ - db36287: feat: chart builder writes back plot-area / chart-area fill + stroke
221
+ colors. A new `spPrChildren(fill, stroke)` helper emits
222
+ `<c:spPr><a:solidFill><a:srgbClr/></a:solidFill><a:ln>…</a:ln>`. The
223
+ builder appends it under `<c:plotArea>` when `plotAreaFill` or
224
+ `plotAreaStrokeColor` is set, and under `<c:chartSpace>` (root) when
225
+ `chartAreaFill` or `chartAreaStrokeColor` is set. Round-trip test
226
+ verifies all four survive.
227
+ - 02434bb: feat: chart builder writes back value-axis extras and tick marks.
228
+ `<c:valAx>` now emits `<c:scaling><c:logBase>`, `<c:majorTickMark>`,
229
+ and `<c:dispUnits><c:builtInUnit>` when authored on `ChartSpec`;
230
+ `<c:catAx>` emits `<c:majorTickMark>`. Round-tripping a chart with
231
+ these fields no longer drops them. Covered by a new round-trip test
232
+ in `fn-chart-readback`.
233
+ - 0afa65b: feat: chart builder writes back full value-axis scaling. `<c:valAx>`
234
+ now emits `<c:scaling><c:min/>/<c:max/>`, `<c:numFmt formatCode>`,
235
+ `<c:majorUnit>`, and `<c:minorUnit>` when authored — completing the
236
+ read/write parity for `ChartSpec.valueAxis`. Round-trip test added.
237
+ - 7bee159: feat: chart builder writes back axis titles, hidden flags, and
238
+ category-axis tick-label config. `<c:valAx>` / `<c:catAx>` now emit:
239
+
240
+ - `<c:title>` with style (from `valueAxisTitleStyle` /
241
+ `categoryAxisTitleStyle`) when an axis title is authored
242
+ - `<c:delete val="1"/>` when `valueAxisHidden` / `categoryAxisHidden`
243
+ - `<c:tickLblPos>` and `<c:tickLblSkip>` when authored on the
244
+ category axis
245
+
246
+ Closes the read/write gap for these `ChartSpec` fields. Round-trip
247
+ test added.
248
+
249
+ - bddd838: feat: chart builder writes back chart-level data-label config. A new
250
+ `dLblsElement` helper builds `<c:dLbls>` with `showVal` / `showCatName`
251
+ / `showSerName` / `showPercent` toggles plus optional `<c:numFmt>`,
252
+ `<c:dLblPos>`, and `<c:separator>`. Wired into every chart variant
253
+ (bar / column / line / pie / doughnut / area), so round-tripping a
254
+ chart with authored data labels preserves them.
255
+ - 3f6f848: feat: chart builder writes back per-data-point `<c:dPt>` overrides.
256
+ New `dPtElements(colors, explosions)` helper emits sparse
257
+ `<c:dPt><c:idx><c:bubble3D val="0"/>[<c:explosion>]
258
+ [<c:spPr><a:solidFill><a:srgbClr/>]</c:dPt>` entries from
259
+ `ChartSeries.pointColors` and `ChartSeries.pointExplosions`.
260
+ Round-trip test asserts both sparse arrays survive.
261
+ - 8a9bab4: feat: chart builder writes back a wide slate of optional chart fields.
262
+ The chart-builder now emits, when authored on `ChartSpec`:
263
+
264
+ - `<c:varyColors>` (per chart kind), `<c:gapWidth>`, `<c:overlap>`
265
+ on bar / column
266
+ - `<c:grouping>` honors `ChartSpec.grouping` (`'clustered' | 'stacked'
267
+ | 'percentStacked' | 'standard'`) on bar / column / line / area
268
+ - `<c:dropLines>`, `<c:hiLowLines>` on line
269
+ - `<c:firstSliceAng>` on pie / doughnut; `<c:holeSize>` on doughnut
270
+ honors `holeSizePct`
271
+ - `<c:majorGridlines>` (with optional `<c:spPr><a:ln><a:solidFill>`
272
+ color), `<c:minorGridlines>` on the value axis
273
+ - `<c:title><c:overlay>` honoring `titleOverlay`
274
+
275
+ Round-trip test asserts the additions all survive read → save →
276
+ reload.
277
+
278
+ - e7078d7: feat: chart builder writes back legend.textStyle + axis orientation
279
+ reversals. `<c:legend><c:txPr>` now carries the authored font / color
280
+ from `legend.textStyle` (via the existing `axisTxPrElement` helper);
281
+ `<c:scaling><c:orientation>` honors `categoryAxisOrientation` and
282
+ `valueAxisOrientation` (defaulting to `minMax`). Round-trip test
283
+ asserts all four survive read → save → reload.
284
+ - 034207d: feat: chart builder writes back `<c:legend>` and `<c:dispBlanksAs>`.
285
+ The chart-root previously emitted only the default legend / blanks
286
+ behavior; the builder now:
287
+
288
+ - emits `<c:legend>` with `legendPos`, `overlay`, and one
289
+ `<c:legendEntry><c:idx><c:delete val="1"/></c:legendEntry>` per
290
+ hidden series index — or skips the element when
291
+ `spec.legend.position === null` (author wants no legend)
292
+ - threads `spec.dispBlanksAs` (`'gap' | 'zero' | 'span'`) into
293
+ `<c:dispBlanksAs>`
294
+
295
+ Round-trip test added.
296
+
297
+ - f643b29: feat: chart builder writes back per-series `<c:dLbls>` overrides.
298
+ `dLblsElement` is refactored to take the labels arg directly via
299
+ `buildDLblsFromLabels(dl)`; `seriesElement` now emits per-series
300
+ `<c:dLbls>` when authored, so charts with per-series label toggles /
301
+ numberFormat / position survive round-trip. Round-trip test covers
302
+ all four fields plus the no-override case.
303
+ - cf2d02b: feat: chart builder writes back series-level optional fields. Each
304
+ `<c:ser>` now emits:
305
+
306
+ - richer `<c:spPr>` with `<a:ln w="…"><a:prstDash/>` when
307
+ `series.lineWidthEmu` or `lineDash` is authored
308
+ - `<c:invertIfNegative val="1"/>` when set
309
+ - `<c:marker><c:symbol/><c:size/></c:marker>` from `markerSymbol` /
310
+ `markerSizePt`
311
+ - `<c:smooth val="1"/>` when set
312
+
313
+ Round-trip test covers all five fields.
314
+
315
+ - 7f0b8ee: feat: chart builder writes back `ChartSpec.titleStyle`. Previously the
316
+ reader picked up authored `<a:rPr sz/b/i><a:solidFill>` on chart
317
+ titles but the builder dropped any incoming style, so round-tripping
318
+ (read → save → reload) lost the title font / color. The builder now
319
+ emits `<a:rPr>` attributes and an inner `<a:solidFill><a:srgbClr/>`
320
+ when a `titleStyle` is provided. New round-trip test
321
+ (`fn-chart-readback`) covers this; total tests 801.
322
+ - bf62577: feat: chart builder writes back per-series `<c:trendline>`. A new
323
+ `trendlineElement(tl)` helper emits `<c:trendlineType>`,
324
+ `<c:period>` (movingAvg), `<c:order>` (poly), `<c:forward>` /
325
+ `<c:backward>`, and `<c:spPr><a:ln><a:solidFill>` color when
326
+ authored. Closes the read/write gap for `ChartSeries.trendline`;
327
+ round-trip test covers type / period / forward / backward / color.
328
+ - 925fd6f: feat: chart builder writes back axis tick-label style + rotation via
329
+ `<c:txPr>`. New `axisTxPrElement(style, rotationDeg)` helper emits the
330
+ `<c:txPr><a:bodyPr rot/><a:lstStyle/><a:p><a:pPr><a:defRPr…/></a:pPr></a:p></c:txPr>`
331
+ payload from `categoryAxisLabelStyle` / `categoryAxisLabelRotationDeg`
332
+ and the value-axis counterparts. Closes the read/write gap for these
333
+ fields; round-trip test added.
334
+ - ba80399: feat: `ChartSpec.categoryAxisMajorGridlines` and
335
+ `ChartSpec.categoryAxisMinorGridlines` — companions to the existing
336
+ `valueAxis*` pair. Bar charts (where the category axis sits on the
337
+ vertical edge) actually use these as horizontal guide lines per
338
+ category band. Mapped to `<c:catAx><c:majorGridlines/>` /
339
+ `<c:minorGridlines/>`. Read by chart-reader, written by chart-builder
340
+ in the correct CT_CatAx schema order (right after `<c:axPos>`).
341
+ - 2d61d26: feat: `ChartSpec.categoryAxisLabelOffset` and
342
+ `ChartSpec.categoryAxisLabelAlign` — two more category-axis tuning
343
+ knobs from ECMA-376. `<c:catAx><c:lblOffset val="N"/>` (0..1000, default 100) controls the distance from the axis line to the labels as a
344
+ percent of text size; `<c:catAx><c:lblAlgn val="ctr|l|r"/>` controls
345
+ how multi-line category labels align relative to their tick mark. Both
346
+ are read by chart-reader and written by chart-builder in the correct
347
+ CT_CatAx schema order.
348
+ - 21f58cb: feat: `ChartSpec.categoryAxisNoMultiLevelLabel` — toggle multi-level
349
+ (hierarchical) category labels via `<c:catAx><c:noMultiLvlLbl val/>`.
350
+ PowerPoint defaults to `0` (multi-level labels stack); set to `true`
351
+ to flatten hierarchical categories into a single row. Read by
352
+ chart-reader, written by chart-builder at the schema-required last
353
+ position inside `<c:catAx>`.
354
+ - c561df4: feat: `ChartSpec.categoryAxisNumberFormat` — number-format code for the
355
+ category-axis tick labels (`<c:catAx><c:numFmt formatCode="…"/>`). Most
356
+ useful on date-style categories (`"mm/dd/yyyy"`, `"mmm-yyyy"`) but
357
+ accepts any Excel format string. Independent of `valueAxis.numberFormat`.
358
+ Read by chart-reader, written by chart-builder in the correct CT_CatAx
359
+ schema order (after `<c:title>`, before `<c:majorTickMark>`).
360
+ - 3ecc11b: feat(chart): category-axis label-skip + position. `ChartSpec.categoryAxisTickLabelSkip`
361
+ reads `<c:catAx><c:tickLblSkip val="N"/>` (render every Nth label),
362
+ and `categoryAxisTickLabelPos` reads `<c:tickLblPos val="…"/>`
363
+ (`'none'` hides labels but keeps the axis line; `'low'`/`'high'`/
364
+ `'nextTo'` are the other tokens). Playground honors both — dense
365
+ time-series charts with `tickLblSkip="5"` no longer overlap their
366
+ labels.
367
+ - e04ccff: feat: `ChartSpec.dataLabels` carries the chart-level `<c:dLbls>` toggles
368
+ — `showValue`, `showCategory`, `showSeriesName`, `showPercent` — read
369
+ from each plotted-kind element. Playground projects them onto bar /
370
+ column tops (numeric value above each bar) and pie / doughnut slices
371
+ (value, percent, and / or category text painted at the slice mid-arc).
372
+
373
+ Adds the `ChartDataLabels` interface to the public type surface.
374
+
375
+ - 199031b: feat: chart data labels honor `<c:dLbls><c:numFmt formatCode="…"/>`.
376
+ `ChartDataLabels.numberFormat` exposes the format code on both
377
+ chart-level and per-series toggle groups, and the playground renderer
378
+ projects value labels through the same Excel-format subset the value
379
+ axis already supports (`"0%"`, `"$#,##0"`, `"0.00"`, etc). Per-series
380
+ formats win over the chart-level default.
381
+ - b77c0ed: feat(chart): `ChartSpec.dispBlanksAs` reads `<c:dispBlanksAs>`
382
+ (`'gap' | 'zero' | 'span'`). Playground line / area renderer:
383
+
384
+ - `gap` (default): breaks the path on null values
385
+ - `zero`: substitutes 0 so the line dips to the baseline
386
+ - `span`: connects the surrounding points across the gap
387
+
388
+ Previously every null value was coerced to 0, which silently
389
+ flattened the chart whenever the deck had genuine missing data.
390
+
391
+ - 0eee0da: feat: `ChartDataLabels.textStyle` — the default-run text style for chart
392
+ data labels is now read and written. `<c:dLbls><c:txPr><a:defRPr/>`
393
+ is parsed into `ChartTextStyle` (sizePt / bold / italic / color) and
394
+ emitted in CT_DLbls schema order (after `<c:numFmt>`, before
395
+ `<c:dLblPos>`). Both the chart-level `dataLabels` and per-series
396
+ `series[i].dataLabels` honor the field.
397
+ - 17f57b3: feat: `ChartSeries.pointColors` — sparse map of per-data-point fill
398
+ overrides read from `<c:ser><c:dPt><c:spPr><a:solidFill>`. Pie /
399
+ doughnut decks almost always emit one of these per slice; the playground
400
+ now paints each slice in its authored color (and reflects it in the
401
+ legend swatches) rather than cycling through the accent palette.
402
+ - 4d2cecb: feat(chart): `ChartSpec.dropLines` and `hiLowLines` read
403
+ `<c:dropLines>` and `<c:hiLowLines>` on line / area / stock plots.
404
+ Playground renders drop lines from each first-series data point down
405
+ to the value axis (dashed gray) and hi-low lines as a vertical span
406
+ between the highest and lowest series value at each category
407
+ (solid darker gray). The latter is the canonical OHLC pattern.
408
+ - 263bf52: feat: chart reader now recognises scatter, bubble, radar, stock, and
409
+ (2D / 3D) surface charts and degrades them to the closest already-
410
+ modelled kind so renderers paint something useful instead of the
411
+ "unsupported chart kind" placeholder. Scatter / bubble series read
412
+ their `<c:yVal>` channel; their `<c:xVal>` / `<c:bubbleSize>` are
413
+ not yet surfaced.
414
+ - 0a9236f: feat(chart): `ChartSpec.gapWidthPct` and `overlapPct` read from
415
+ `<c:gapWidth>` and `<c:overlap>` on bar / column plots. Playground
416
+ sizes bars per ECMA-376 §21.2.2.75 — `barW = groupW / (clusterUnits +
417
+ gapWidth/100)` with `clusterUnits = 1 + (S - 1)(1 - overlap/100)` —
418
+ so authored bar spacing matches PowerPoint instead of the hard-coded
419
+ 0.8 / 0.7 ratios.
420
+ - b88dbb8: feat(chart): `ChartSpec.valueAxisMajorGridlines` / `valueAxisMinorGridlines`
421
+ read the presence of `<c:majorGridlines/>` / `<c:minorGridlines/>`
422
+ under `<c:valAx>`. Playground hides gridlines when `majorGridlines`
423
+ is explicitly `false` (absent in the source XML) — common on KPI
424
+ charts that show clean bars / lines without horizontal rules behind
425
+ them. Tick labels still render.
426
+ - 4caa5ad: feat(chart): `ChartSeries.invertIfNegative` reads `<c:ser>
427
+ <c:invertIfNegative val="1"/>`. Playground's bar / column renderer
428
+ paints negative bars in a darker shade of the series color when the
429
+ flag is set — matching PowerPoint's profit/loss visualization.
430
+ - b603115: feat: `ChartSpec.language` (`<c:chartSpace><c:lang val=…/>`) and
431
+ `ChartSpec.date1904` (`<c:date1904 val=…/>`) — chartSpace-level Office
432
+ metadata round-tripped for parity. `language` is the Office UI
433
+ language code (e.g. `'en-US'`, `'ja-JP'`); `date1904` selects the
434
+ 1904 date epoch (default `false` = Excel 1900 epoch, surface only
435
+ when explicitly true). pptx-kit's renderers don't act on either yet.
436
+ - 028e3b7: feat: chart `<c:legend><c:legendEntry><c:delete val="1"/>` honored.
437
+ `ChartSpec.legend.hiddenIndices` carries the series indices the
438
+ author wants suppressed from the legend (typically trendline series).
439
+ The playground filters the parallel legend arrays (names, colors,
440
+ marker glyphs) in lock-step so the remaining entries stay aligned,
441
+ without affecting plotted data.
442
+ - c141173: feat(chart): `ChartSpec.legend` carries the `<c:legend><c:legendPos>`
443
+ token — `'r' | 't' | 'b' | 'l' | 'tr'`. Playground projects each
444
+ onto the appropriate edge (horizontal row for top / bottom, vertical
445
+ stack for the side / corner positions). Charts whose `<c:legend>`
446
+ sets `position` to `null` paint without a legend.
447
+ - 9a49faf: feat(chart): `ChartAxisScaling.majorUnit` and `minorUnit` read
448
+ `<c:valAx><c:majorUnit>` / `<c:minorUnit>` tick spacing. Playground's
449
+ value-axis renderer emits ticks at each multiple of the authored
450
+ majorUnit instead of nice-rounded auto-ticks when present.
451
+ - f99d548: feat: `ChartSpec.valueAxisMinorTickMark` and `categoryAxisMinorTickMark`
452
+ — minor-tick-mark mode siblings of the existing `*MajorTickMark` pair.
453
+ Maps to `<c:catAx><c:minorTickMark val="in|out|cross|none"/>` and the
454
+ value-axis equivalent. Read by chart-reader, written by chart-builder
455
+ in the correct schema order (right after `<c:majorTickMark>`).
456
+ - 28d77ea: fix: chart categories accept `<c:cat><c:numRef>` (numeric / date
457
+ categories). Previously the category-axis dropped to an empty
458
+ labels array when the chart authored a numeric category channel
459
+ (common for date-axis line charts authored in Excel). Falls back
460
+ to formatting each cached numeric value as a string so date /
461
+ number cats appear on the axis instead of disappearing.
462
+ - 7b3ba0a: feat(chart): axis number formats now accept Excel's `"$"#,##0`
463
+ quoted-literal prefix / suffix syntax. PowerPoint typically emits
464
+ currency as `"$"#,##0` (or `"\$"#,##0`) rather than the bare `$`
465
+ form, so the previous detection missed it.
466
+ - d2f86d2: feat(chart): `ChartAxisScaling.numberFormat` reads `<c:valAx><c:numFmt
467
+ formatCode="…"/>`. Playground projects the most common Excel format
468
+ codes to axis labels — percent (`'0%'`, `'0.0%'`), thousand
469
+ separator (`'#,##0'`, `'#,##0.0'`), and currency prefixes
470
+ (`'$#,##0'`, `'¥#,##0'`). Other codes fall through to the generic
471
+ auto-formatted label.
472
+ - 3efdbeb: feat(chart): `ChartSpec.titleOverlay` and `ChartSpec.legend.overlay`
473
+ read `<c:title><c:overlay>` / `<c:legend><c:overlay>`. When `true`,
474
+ the title / legend sits on top of the plot area instead of taking a
475
+ horizontal strip. Playground sizes the plot area accordingly — gives
476
+ the chart back the extra vertical real estate when overlay is set.
477
+ - 4cde872: feat: `ChartSpec.plotVisibleCellsOnly` — toggle `<c:plotVisOnly val/>`.
478
+ PowerPoint's default is `true` (only plot visible cells); the field
479
+ exists to let authors opt into `false` (plot hidden rows / columns too).
480
+ The reader surfaces `false` only when the wire is explicitly `0` so
481
+ round-tripping the common default doesn't drag a redundant explicit
482
+ `true` into the spec.
483
+ - 693ba3e: feat: `ChartSpec.roundedCorners` — round-trip the chartSpace-level
484
+ `<c:roundedCorners val>` toggle. PowerPoint's default is `false`; the
485
+ reader surfaces `true` only when the wire is explicitly `1` and the
486
+ builder emits the element only when authored, so common defaults stay
487
+ clean. Schema position is BEFORE `<c:chart>` (per CT_ChartSpace).
488
+ - 733120a: feat(chart): `ChartSeries.smooth` reads `<c:smooth val="1"/>`. Playground
489
+ line / area renderer interpolates a cubic-Bézier curve through the
490
+ data points (Catmull-Rom-to-Bezier with 0.5 tension) when `smooth` is
491
+ true, matching PowerPoint's "smooth line" preset visually.
492
+ - d581121: feat(site/playground): bar (horizontal), line, and area charts now
493
+ honour `ChartSpec.grouping` for stacked / percentStacked layouts —
494
+ matching the column-chart treatment added previously. Data labels
495
+ render inside the stacked segments for bar (white bold), at the
496
+ appropriate cumulative position for line / area, and percent-stacked
497
+ versions normalize each category to 100%.
498
+ - 33c2c11: feat: `ChartSpec.grouping` carries the `<c:grouping>` token —
499
+ `'clustered' | 'stacked' | 'percentStacked' | 'standard'`. Playground
500
+ column chart renders stacked / percent-stacked layouts: series stack
501
+ within each category, and percent-stacked normalises to 0..100% per
502
+ column with in-bar value labels.
503
+
504
+ Adds the `ChartGrouping` type to the public surface.
505
+
506
+ - 53148e4: feat: `ChartSpec.chartStyle` — round-trip the chartSpace-level
507
+ `<c:style val="N"/>` PowerPoint chart-style preset (1..48). Encodes a
508
+ curated combo of theme accent colors, gradients, effects, and font
509
+ sizes from the PowerPoint "Chart Styles" gallery. Read and written for
510
+ round-trip parity; pptx-kit's renderers don't interpret the preset
511
+ yet, but the field survives save/reload.
512
+ - b1cfda3: feat: `ChartSpec.categoryAxisTickMarkSkip` — the second half of the
513
+ ECMA-376 `<c:catAx>` skip pair. `<c:tickLblSkip>` (already supported)
514
+ controls label-skip stride; `<c:tickMarkSkip val="N"/>` independently
515
+ draws every Nth tick mark. Useful when you want fewer label collisions
516
+ but the same dense tick lattice. Read by chart-reader and written by
517
+ chart-builder.
518
+ - 2599a46: feat: chart titles read `<c:tx><c:strRef>` workbook-cell references.
519
+ Previously only literal `<c:rich>` titles surfaced; titles authored
520
+ via Excel's "Link to source cell" wizard (which emits `<c:strRef>`
521
+ with a `<c:strCache>` of the resolved text) now flow through to
522
+ `ChartSpec.title` as the cached value. Affects the title shown above
523
+ the chart and, transitively, axis-title rendering.
524
+ - da1e50d: feat: chart titles honor `<a:rPr>` font / color overrides.
525
+ `ChartSpec.titleStyle` carries the authored size (in pt), bold, italic,
526
+ and fill color extracted from the title's first `<a:r><a:rPr>` (or
527
+ `<a:pPr><a:defRPr>` as fallback). The playground renderer projects
528
+ those through to the SVG `<text>`. Templates that brand their chart
529
+ titles to a non-default size / color finally render with the authored
530
+ look.
531
+ - 5f84cfc: feat: `ChartTrendline.displayEquation` and `ChartTrendline.displayRSquared`
532
+ — two booleans that toggle the regression-equation label and R²
533
+ coefficient overlay next to a trendline. Map to
534
+ `<c:trendline><c:dispEq val="1"/>` and `<c:dispRSqr val="1"/>`. Read by
535
+ chart-reader; written by chart-builder in the correct CT_Trendline
536
+ schema order (after `<c:backward>`, before any `<c:trendlineLbl>`).
537
+ - a978251: feat: `ChartTrendline.name` — round-trip a custom trendline label
538
+ (`<c:trendline><c:name>…`). PowerPoint auto-generates a label like
539
+ "Linear (X)" or "MA(5) (X)" when this element is omitted; authors who
540
+ want a different label (or who imported one from another tool) now
541
+ have the field. Read by chart-reader; written by chart-builder at the
542
+ CT_Trendline schema-required first position (before `<c:spPr>`).
543
+ - 57eeffa: feat(chart): `ChartSeries.trendline` reads `<c:trendline>` —
544
+ regression type (linear / exp / log / poly / power / movingAvg),
545
+ moving-average period, polynomial order, and the trendline's stroke
546
+ color. Playground overlays a dashed trendline on bar / column / line
547
+ charts; linear / log / exp use fitted regressions, movingAvg uses a
548
+ rolling mean.
549
+
550
+ Adds the `ChartTrendline` type to the public surface.
551
+
552
+ - 24b2794: feat: `ChartSpec.valueAxisCrossBetween` — controls whether the value
553
+ axis crosses the category axis _between_ tick marks (the default for
554
+ bar/column/area) or _at_ each tick mark (the default for line/scatter).
555
+ Maps to `<c:valAx><c:crossBetween val="between|midCat"/>`. Read by
556
+ chart-reader, written by chart-builder in the correct CT_ValAx schema
557
+ order (after `<c:crossesAt>`).
558
+ - 6a236be: feat: `ChartSpec.valueAxisCrosses` — controls where the category axis
559
+ crosses the value axis. Accepts either an enum keyword
560
+ (`'autoZero' | 'min' | 'max'` → `<c:valAx><c:crosses val=…/>`) or a
561
+ numeric tagged form (`{ at: N }` → `<c:valAx><c:crossesAt val=N/>`).
562
+ The two forms are mutually exclusive per the schema; `crossesAt` wins
563
+ when both are present on read. Read by chart-reader, written by
564
+ chart-builder in the correct CT_ValAx schema order (after `<c:crossAx>`).
565
+ - b2c1304: feat: chart `<c:varyColors>` for single-series bar / column.
566
+ `ChartSpec.varyColors` carries the `<c:plottedKind><c:varyColors val="1"/>`
567
+ flag. When set and the chart has exactly one series, the renderer
568
+ assigns each data point a distinct accent color (mirroring
569
+ PowerPoint's "Vary colors by point" toggle for column / bar). Pies
570
+ already varied colors implicitly.
571
+ - 69431a9: feat: `getSlideColorMapOverride(slide)` returns the slide's
572
+ `<p:clrMapOvr><a:overrideClrMapping/>` token-remap, or `null` when the
573
+ slide inherits the master's color map. Returned as a plain `Record`
574
+ with the eight stable tokens (`bg1`, `tx1`, `bg2`, `tx2`, `accent1`-
575
+ `accent6`, `hlink`, `folHlink`) keyed to their override targets.
576
+ Useful for renderers that need to know when a slide reinterprets the
577
+ theme's color story.
578
+ - 263bf52: feat: apply ECMA-376 §20.1.2.3.x color transforms when resolving colors.
579
+
580
+ - New `resolveDrawingColor(colorEl, theme)` resolves any DrawingML color
581
+ element (`<a:srgbClr>` / `<a:schemeClr>` / `<a:sysClr>` / `<a:prstClr>`)
582
+ with all transform children (`<a:lumMod>`, `<a:lumOff>`, `<a:shade>`,
583
+ `<a:tint>`, `<a:satMod>` / `Off`, `<a:hueMod>` / `Off`, `<a:gray>`,
584
+ `<a:inv>`, `<a:comp>`) applied. Scheme tokens are looked up against
585
+ the supplied theme.
586
+ - New `getShapeFillColorResolved(pres, shape)` and
587
+ `getShapeStrokeColorResolved(pres, shape)` return the exact `#RRGGBB`
588
+ PowerPoint paints — useful for renderers / exporters where the legacy
589
+ `getShapeFillColor` / `getShapeStrokeColor` strings (`#RRGGBB` or
590
+ `scheme:<token>`) miss both scheme resolution and color transforms.
591
+ - `getShapeRunFormatEffective` now applies the same pipeline at every
592
+ layer of the rPr cascade, so a run inheriting `accent1 lumMod=40000
593
+ lumOff=60000` (PowerPoint's "Accent 1, Lighter 60%") resolves to the
594
+ concrete tinted hex instead of leaking the raw token through.
595
+
596
+ - 263bf52: feat(site/playground): bent / curved connector routing.
597
+
598
+ `bentConnector{2,3,4,5}` render as the matching L / Z / two-step /
599
+ three-step paths, and `curvedConnector{2,3,4,5}` render as quadratic
600
+ / cubic Bézier curves between the connector's bounding-box endpoints.
601
+ Previously every connector preset projected to a straight line; flow-
602
+ chart and diagram decks now show the right cadence.
603
+
604
+ - e65228f: feat: read custom geometry. New `getShapeCustomGeometry(shape)` returns a
605
+ shape's `<a:custGeom>` (ECMA-376 §20.1.9) as a fully-evaluated path list —
606
+ guide formulas (`avLst`/`gdLst`, all §20.1.9.11 operators) are resolved
607
+ against the shape extents so the returned `moveTo`/`lnTo`/`arcTo`/
608
+ `quadBezTo`/`cubicBezTo`/`close` commands carry only numbers. The preview
609
+ renderer now draws custom geometry as a real SVG path (arcs converted to
610
+ cubic Béziers) instead of a labelled rectangle placeholder; only a custGeom
611
+ that fails to evaluate still falls back, marked `data-pptx-fallback`.
612
+ - 1e774b8: feat(site/playground): pie / doughnut / line / area honor
613
+ `<c:dLblPos>` data-label positions. Pie supports `ctr` (default
614
+ midline), `inEnd` (just inside the rim), and `outEnd` (outside the
615
+ pie, with a darker fill so it shows on the chart-area backdrop). Line
616
+ and area chart per-point labels honor `ctr`, `t` (default), `b`, `l`,
617
+ `r`. Column / bar already shipped in the prior batch.
618
+ - fc019d1: feat: chart data label position. `ChartDataLabels.position` carries the
619
+ `<c:dLbls><c:dLblPos val="…"/>` token (typed as
620
+ `ChartDataLabelPosition`). The reader extracts it at both chart-level
621
+ and per-series scope. The playground renderer projects `ctr`, `inEnd`,
622
+ `outEnd`, `inBase` onto clustered column and bar labels — outside-end
623
+ remains the default, but authored positions now move labels inside the
624
+ bar or to the base as PowerPoint shows them.
625
+ - 3cfba8d: feat: chart data label separator. `ChartDataLabels.separator` carries
626
+ the `<c:dLbls><c:separator>…</c:separator>` text used to join
627
+ multiple label parts (value + percent + category etc.). The pie /
628
+ doughnut renderer threads the per-series override, falling back to
629
+ the chart-level separator and finally to a single space. Common
630
+ values: `", "`, `"\n"`, `"; "`.
631
+ - 003e7b5: feat: density-array companions for tables and images —
632
+ `getPresentationTableCountsBySlide(pres)` and
633
+ `getPresentationImageCountsBySlide(pres)`. Both return a dense
634
+ per-slide count array (0 for slides without that asset kind),
635
+ matching the shape / chart / comment / text-length counterparts.
636
+ Completes the deck-density family.
637
+ - fd1519a: feat(site/playground): `<c:dispUnits>` value-axis label. When the
638
+ chart authors a display-units token (`thousands`, `millions`, etc.)
639
+ the value-axis now emits an italic "Thousands" / "Millions" /
640
+ … label rotated alongside the axis (vertical orientation) or to
641
+ the right of the rightmost tick (horizontal). Completes the
642
+ display-units rendering — values are already divided, and now the
643
+ scale self-describes.
644
+ - c5f0b60: feat: chart value-axis honors `<c:dispUnits><c:builtInUnit/>`.
645
+ `ChartAxisScaling.displayUnits` carries the authored scale token
646
+ (`hundreds`, `thousands`, `millions`, etc.). The playground divides
647
+ each value-axis tick by the corresponding divisor before formatting,
648
+ so charts authored "in millions" finally render as `10` / `20` /
649
+ `30` instead of `10000000`.
650
+ - 263bf52: feat: add `getShapeRunFormatEffective(pres, shape, p, r)` — resolves a
651
+ run's character properties (font, size, color, bold, italic, underline)
652
+ through the full ECMA-376 §21.1.2.4.7 inheritance chain: run `<a:rPr>` →
653
+ `<a:endParaRPr>` → paragraph `<a:defRPr>` → text-body `<a:lstStyle>` →
654
+ matching layout placeholder → matching master placeholder → master
655
+ `<p:txStyles>` → theme `<a:fontScheme>`. Theme tokens like `+mj-lt` are
656
+ expanded to the deck's major/minor typefaces. The existing
657
+ `getShapeRunFormat` still returns the literal `<a:rPr>` only.
658
+ - 25654cf: feat: `getShapeEffectsEffective(pres, shape)` walks the layout →
659
+ master placeholder cascade for `<a:effectLst>`. Effect lists override
660
+ rather than compose (matching PowerPoint's behaviour), so the first
661
+ layer that supplies any effects wins. Playground uses it so
662
+ placeholder shadows / glows / soft edges inherited from the master
663
+ finally render on slides that don't repeat the effect list.
664
+ - acc9b15: feat: effects & fills polish. The reflection effect (`a:reflection`) now
665
+ renders as a vertically mirrored, gradient-masked copy honoring start/end
666
+ alpha, distance, and the signed `sy` scale; picture bullets (`a:buBlip`)
667
+ render as real inline images in both text layout modes via the new core
668
+ reader `getParagraphBulletImageBytes` (the "■" fallback remains only when
669
+ bullet bytes are genuinely unavailable); and gradient fills inherited
670
+ through the placeholder layout/master cascade resolve via the new
671
+ `getShapeGradientFillEffective` instead of painting a hardcoded orange
672
+ tint.
673
+ - 263bf52: feat: `getShapeEffects(pres, shape)` returns every effect on the
674
+ shape's `<a:effectLst>` (`outerShdw`, `innerShdw`, `glow`, `reflection`,
675
+ `softEdge`, `blur`) in document order, with each effect's color
676
+ (transform-resolved against the theme), opacity, blur radius, distance,
677
+ and angle. PowerPoint composes multiple effects in a single filter
678
+ stack — the existing `getShapeEffect` only surfaced the first one.
679
+
680
+ The playground renderer now emits an SVG `<filter>` chain that
681
+ composes the same effects, including a synthesized inner shadow
682
+ (SVG has no `feInnerShadow` primitive — built via offset + composite).
683
+
684
+ Also adds the `ShapeEffectAny` type union to the public surface.
685
+
686
+ - 18d3ceb: feat: `getShapeFillEffective(pres, shape)` walks the layout → master
687
+ placeholder cascade when the shape's own fill is `'inherit'`. Returns
688
+ the first non-inherit fill found. Playground reaches for it as its
689
+ primary fill source so placeholder fills authored on the master /
690
+ layout finally show through.
691
+ - 81ce680: feat: `findShapesByPreset(slide, preset)` returns every shape whose
692
+ `<a:prstGeom prst="…"/>` matches. Useful for diagram introspection:
693
+ find all `'leftArrow'`s for a workflow swap, replace every `'cloud'`
694
+ with `'rect'`, etc. Shapes without a preset (custGeom / pictures /
695
+ charts / tables / connectors / groups) are filtered out.
696
+ - 019a934: feat: `findChartsWithDataLabels(slide)` — slide-scoped auditor for
697
+ charts whose chart-level or per-series `dataLabels` enable at least
698
+ one of `showValue` / `showCategory` / `showSeriesName` / `showPercent`.
699
+ Purely presence-based; doesn't validate numberFormat or position.
700
+ Charts whose kind isn't modeled are skipped.
701
+ - cb5d037: feat: `findChartsWithTrendlines(slide)` — slide-scoped finder for
702
+ charts that carry at least one `<c:trendline>` on any of their
703
+ series. Useful for deck-audit reports — trendlines are easy to add
704
+ and easy to forget. Charts whose kind isn't modeled are skipped.
705
+ - 1653804: feat: `findCommentsByAuthor(pres, authorName)` and
706
+ `findSlidesWithCommentsByAuthor(pres, authorName)` now accept a
707
+ `RegExp` as well as a literal string. Useful for "every comment from
708
+ review bots" (`/^review-bot/`) or "every comment from anyone with a
709
+ given email domain" patterns. Backward compatible — string callers
710
+ still get exact-equality matching.
711
+ - b6d9ea4: feat: `findShapeByName(slide, name)` now accepts a `RegExp` as well
712
+ as a literal string. Mirrors the RegExp support just landed on
713
+ `findShapesByName` (multi-match). Returns the first match in document
714
+ order; backward compatible with existing string callers.
715
+ - 7e59ac4: feat: `findShapeInPresentation(pres, name)` now accepts a `RegExp` as
716
+ well as a literal string. Mirrors the RegExp support on the
717
+ slide-scoped `findShapeByName`. Backward compatible — string callers
718
+ still get exact-equality.
719
+ - 70a2327: feat: `findShapesByEffect(pres, slide, kind)` — returns every shape on
720
+ the slide whose `<a:effectLst>` carries an effect of the given `kind`
721
+ (`'outerShdw'`, `'innerShdw'`, `'glow'`, `'reflection'`, `'softEdge'`,
722
+ `'blur'`). Pure presence check; doesn't walk the layout / master
723
+ cascade. Useful for "which shapes have a shadow / glow on this
724
+ slide?" audits.
725
+ - 94c4480: feat: `findShapesByHyperlink(slide, url)` — slide-scoped finder that
726
+ returns every shape whose hyperlink target matches `url` (substring or
727
+ `RegExp`). Pairs the existing presentation-level
728
+ `findSlidesByHyperlink` for cases where the caller already has a
729
+ specific slide and wants the linking shapes inside it.
730
+ - e71664d: feat: `findShapesByName(slide, name)` now accepts a `RegExp` as well
731
+ as a literal string. Useful when template-cloned shapes share a
732
+ prefix (`'TextPlaceholder1'`, `'TextPlaceholder2'`, …). Backward
733
+ compatible — string callers still get exact-equality matching.
734
+ - f57a4ab: feat: `findShapesInRect(slide, x, y, w, h)` — marquee-style region
735
+ finder. Returns every shape whose bounds overlap the rectangle
736
+ (touching edges count). Shapes with no resolvable bounds are skipped.
737
+ Companion to `findShapesAtPoint(slide, x, y)` for cases where the
738
+ caller wants a region of the slide rather than a single point.
739
+ - a7c00cb: feat: `findShapesWithAnimation(slide)` — returns every shape on the
740
+ slide whose `getShapeAnimation` is not `null`. Pair to
741
+ `slideHasAnimations`. Useful for "which shapes on this slide actually
742
+ animate?" audits before exporting to a video pipeline that doesn't
743
+ honor PowerPoint's timing tree.
744
+ - 87d7fbb: feat: `findShapesWithHyperlinks(slide)` — every shape on the slide
745
+ that carries any hyperlink, regardless of target. Counterpart to
746
+ `findShapesByHyperlink(slide, url)` (which requires a matching URL)
747
+ for "audit every clickable shape on this slide" workflows.
748
+ - 5cc4f75: feat: `findSlideByTitle(pres, title)` now accepts a `RegExp` as well
749
+ as a literal string. Pairs the RegExp support on
750
+ `findSlidesByText` / `findShapeByName` / `findCommentsByAuthor`.
751
+ Backward compatible — string callers still get exact-equality.
752
+ - 134943d: feat: `findSlidesByLayoutPartName(pres, layoutPartName)` — finds every
753
+ slide whose resolved layout part name matches `layoutPartName` (e.g.
754
+ `'/ppt/slideLayouts/slideLayout3.xml'`). Pair to the existing
755
+ `findSlidesByLayoutName` / `findSlidesByLayoutType`. Keyed on the
756
+ actual package path, so it's stable across template-name collisions
757
+ and PowerPoint UI locales.
758
+ - 20613b5: feat: `findSlidesWithChartKind(pres, kind)` — kind-filtered variant of
759
+ the existing `getSlidesWithCharts`. Returns every slide carrying at
760
+ least one chart of the given `ChartKind` (`'bar'`, `'column'`,
761
+ `'line'`, `'pie'`, `'doughnut'`, `'area'`). Built on `getSlideCharts`
762
+ so the predicate respects the spec the renderers actually see.
763
+ - 7c545c9: feat: `findSlidesWithChartTrendlines(pres)` — deck-level variant of
764
+ the slide-scoped `findChartsWithTrendlines`. Returns every slide
765
+ carrying at least one chart with a trendline on any series. Useful
766
+ for "audit every trendline in this deck" workflows before publishing.
767
+ - 666343d: feat: `getEmptySlides(pres)` — returns every slide whose `<p:spTree>`
768
+ carries no shapes (per `getSlideShapes`). Useful as a pre-publish
769
+ "find the section dividers I forgot to fill in" check.
770
+ - b4dbcc0: feat: `getPresentationChartCountsBySlide(pres)` — dense per-slide chart
771
+ count array. Counts every chart returned by `getSlideCharts` regardless
772
+ of whether its spec parsed; pair with `getPresentationChartKindCounts`
773
+ for kind-level totals. Rounds out the density-array family alongside
774
+ `getPresentationCommentCountsBySlide`,
775
+ `getPresentationShapeCountsBySlide`, and
776
+ `getPresentationTextLengthsBySlide`.
777
+ - a4ca6ca: feat: `getPresentationChartKindCounts(pres)` — deck-wide histogram of
778
+ `ChartKind` → count. Returns a frozen `Record` with every kind
779
+ present (zeros for absent kinds), so destructuring and chart-style
780
+ audits stay typed without runtime checks. Charts whose spec doesn't
781
+ parse are skipped, matching `findChartByKind` / `findSlidesWithChartKind`.
782
+ - f7dbcc4: feat: `getPresentationCommentCountsByAuthor(pres)` — deck-wide
783
+ histogram of comment counts keyed by author display name. Useful for
784
+ "who reviewed this deck the most?" audits. Authors sharing a display
785
+ name get merged into the same bucket; pair with
786
+ `getPresentationCommenters` when authors with identical names need to
787
+ be kept separate by `id`.
788
+ - be1a608: feat: `getPresentationCommentCountsBySlide(pres)` — dense per-slide
789
+ comment count array. Every slide appears as an element (count `0`
790
+ when the slide has no comments), so callers can chart comment
791
+ density per slide without re-indexing.
792
+ - 2789bb3: feat: `getPresentationHyperlinkCountsBySlide(pres)` — dense per-slide
793
+ hyperlink count array. Counts shapes whose `getShapeHyperlink` is
794
+ non-null. Cheaper than `getAllHyperlinks` when the caller only wants
795
+ per-slide counts. Rounds out the deck-density family.
796
+ - 2f67bd7: feat: `getPresentationNotesLengthsBySlide(pres)` — dense per-slide
797
+ speaker-notes length array. Pair with
798
+ `getPresentationTextLengthsBySlide` for handout / talk-track audits —
799
+ slides with little on-screen text but heavy notes are usually the
800
+ slow part of a presentation.
801
+ - 1974b01: feat: `getPresentationShapeCountsBySlide(pres)` — dense per-slide
802
+ shape count array. Counts whatever `getSlideShapes` flattens (top-
803
+ level + group-children). Useful for charting shape density per slide
804
+ and identifying outliers for cleanup.
805
+ - 6569f9d: feat: `getPresentationTextLengthsBySlide(pres)` — dense per-slide
806
+ visible-text length array. Counts code points (surrogate pairs as 1)
807
+ per `getSlideTextLength`. Pair with `getPresentationShapeCountsBySlide`
808
+ for slide-density audits.
809
+ - b793c74: feat: `getSlideLayoutUsageCountsByType(pres)` — companion to
810
+ `getSlideLayoutUsageCounts`, but keyed on the OOXML layout-type enum
811
+ token (`title`, `obj`, `twoObj`, `blank`, …) instead of the user-
812
+ visible name. Stable across PowerPoint UI locales. Useful for "how
813
+ many content slides vs. dividers vs. title slides?" audits.
814
+ - 3891fa2: feat: `getSlideLayoutUsageCounts(pres)` — layout name → number-of-slides
815
+ histogram. Every layout enumerated by `getSlideLayouts` appears as a
816
+ key (count `0` for unreferenced layouts), so the function surfaces
817
+ unused layouts directly — useful for trimming template decks that
818
+ ship with placeholder layouts the working deck never picks up.
819
+ - aad46e4: feat: `getSlideMasterUsageCounts(pres)` — master part name → number of
820
+ slides chaining to that master. Every master in the package appears as
821
+ a key (count `0` for unreferenced masters), so it surfaces unused
822
+ masters directly. Pair with `getSlideLayoutUsageCounts` for the
823
+ layout layer in multi-master template decks.
824
+ - 02fe159: feat: `getSlideTables(slide)` — returns every table graphic-frame
825
+ shape on the slide, in document order. Pair to `getSlideCharts` for
826
+ cases where the caller wants just the tables; convenience over
827
+ `getSlideShapes(slide).filter(isTableShape)`.
828
+ - acf5880: feat: `getUnusedSlideLayouts(pres)` — returns the layouts in the
829
+ package that no slide references. Useful when trimming a template
830
+ deck — unused layouts contribute parts and rels without ever
831
+ rendering. Iteration order matches `getSlideLayouts`.
832
+ - eca84ce: feat: `getUnusedSlideMasters(pres)` — master part names that no slide
833
+ chains to (count of `0` in `getSlideMasterUsageCounts`). Pair to
834
+ `getUnusedSlideLayouts`. Useful when trimming multi-master template
835
+ decks of dead theme variants.
836
+ - 263bf52: feat: `getShapeGradientFill` now surfaces non-linear gradient paths
837
+ (`<a:path path="circle|rect|shape">`) and the `<a:fillToRect>` focus
838
+ rectangle. `GradientFillOptions` gains `path` and `focus` fields so
839
+ renderers can reproduce radial, rectangular, and shape-following
840
+ gradients instead of falling back to a linear approximation.
841
+
842
+ The playground renderer emits an SVG `<radialGradient>` for the
843
+ non-linear paths, with reversed stop offsets so the first ECMA-376
844
+ stop sits at the focus center (matching PowerPoint's outward
845
+ painting order).
846
+
847
+ - 855076d: feat: chart value-axis major gridlines honor authored stroke color.
848
+ `ChartSpec.valueAxisMajorGridlineColor` extracts the
849
+ `<c:majorGridlines><c:spPr><a:ln><a:solidFill><a:srgbClr/>` color and
850
+ the playground renderer paints gridlines with it (falls through to the
851
+ existing light-gray default when no color is authored). Branded
852
+ templates with custom gridline tints finally render correctly.
853
+ - 74b227e: feat(site/playground): hyperlink tooltips. Shape and per-run
854
+ hyperlinks now surface their `<a:hlinkClick tooltip="…"/>` text —
855
+ shapes get an SVG `<title>` child on the `<a>` wrapper, runs get a
856
+ `title=` attribute on the HTML anchor. PowerPoint shows these on
857
+ hover during the slideshow; the playground now does too.
858
+ - a610e82: feat: `getShapeHyperlinkTooltip(shape)` and
859
+ `getShapeRunHyperlinkTooltip(shape, p, r)` return the
860
+ `<a:hlinkClick tooltip="…"/>` text. Tooltips show up in PowerPoint
861
+ when the user hovers a linked shape in slide-show mode — useful for
862
+ accessibility and link-preview surfaces.
863
+ - cbdda7c: feat(site/playground): render `<a:duotone>` image recolor. The filter
864
+ pipeline desaturates the picture to luminance, then samples a
865
+ two-color gradient (firstColor → secondColor) via a 16-step
866
+ `feComponentTransfer` table. Pictures with PowerPoint's Color >
867
+ Recolor preset finally render in their authored two-color tint.
868
+ - c4a89c1: feat: `getShapeImageDuotone(pres, shape)` reads the picture's
869
+ `<a:blip><a:duotone>` two-color recolor effect — the typical
870
+ "Picture Tools > Recolor" output. Returns the two hex-resolved
871
+ colors (or `null` for each that the duotone didn't author). Lets
872
+ downstream renderers project the duotone via SVG `<filter>` or
873
+ inform consumers that the picture has a color-replacement applied.
874
+ - 99fcb65: feat: image color-effect readers — `isShapeImageGrayscale(shape)`
875
+ detects `<a:blip><a:grayscl/>` (Color > Grayscale), and
876
+ `getShapeImageBiLevelThreshold(shape)` returns the threshold percent
877
+ for `<a:blip><a:biLevel thresh="…"/>` (Color > Black and White).
878
+ Renderers can project these onto CSS / SVG filters.
879
+ - 263bf52: feat: `getShapeImageLinkUrl(shape)` returns the external URL of a
880
+ picture whose `<a:blip>` carries an `r:link` (Insert > "Link to file")
881
+ instead of `r:embed`. Bytes for these aren't in the package; the
882
+ playground now shows the linked URL in the placeholder rather than a
883
+ generic "no bytes" label.
884
+ - 508627a: feat(site/playground): grayscale + biLevel image filters in the
885
+ playground. The filter pipeline now composes:
886
+
887
+ 1. brightness + contrast (linear feComponentTransfer)
888
+ 2. grayscale (luminance-preserving feColorMatrix) when
889
+ `<a:blip><a:grayscl/>` is set
890
+ 3. biLevel two-tone (discrete tableValues snapped at the authored
891
+ threshold) when `<a:blip><a:biLevel thresh="…"/>` is set
892
+
893
+ Pictures with Color > Grayscale or Color > Black and White now
894
+ render with the same visual treatment PowerPoint shows.
895
+
896
+ - 66edcbc: feat: add `isShapeTextBox(shape)` — `true` when a shape is a text box
897
+ (`<p:cNvSpPr txBox="1">`) rather than an autoshape. The two have different
898
+ default text formatting (text boxes left/top, autoshapes center/middle), so
899
+ renderers and layout code need to tell them apart.
900
+ - 263bf52: feat: `getSlideLayoutBackground(layout)` mirrors `getSlideBackground`
901
+ for slide layouts. Playground falls back to it when the slide's own
902
+ background reports `'inherit'`, so brand-color or template backgrounds
903
+ authored on the layout actually paint behind slides that don't override
904
+ the bg themselves.
905
+ - 2a1d712: feat: `getSlideLayoutBackgroundGradientFill(layout)` returns the
906
+ gradient definition when a layout's background is
907
+ `<p:bgPr><a:gradFill>`. Same shape as the slide-level variant —
908
+ renderers can reuse the same projection logic for layout gradient
909
+ backgrounds via the shared `gradientDef` helper.
910
+ - a1229d5: feat: `getSlideLayoutBackgroundShapes(pres, layout)` returns the
911
+ non-placeholder shapes on a layout as a render-ready view
912
+ (`SlideLayoutBackgroundShape` — bounds, preset, fillHex, strokeHex,
913
+ strokeWidthEmu, rotation, flip). Playground paints them behind the
914
+ slide's own shapes so brand-template decoration (corner bars, divider
915
+ lines, background rectangles) shows through on slides that don't
916
+ redefine the layout themselves.
917
+
918
+ Adds the `SlideLayoutBackgroundShape` type to the public surface.
919
+
920
+ - ba056db: feat: `getSlideLayoutBackground` now handles `<p:bgRef>` the same way
921
+ `getSlideBackground` does. Layouts in real brand templates almost
922
+ always reference the theme via `<p:bgRef>` rather than authoring an
923
+ explicit `<p:bgPr>` — picking up the inner color element as a solid
924
+ fill closes the cascade so the playground paints the right brand
925
+ color even when the slide's own background reports `inherit`.
926
+ - 3ea2ed5: feat(site/playground): line / area chart legend swatches use the
927
+ series marker glyph. The legend previously rendered every series as
928
+ a 9×9 square color rect. For `line` / `area` charts the renderer now
929
+ passes the per-series `markerSymbol` (circle / square / diamond /
930
+ triangle / star / x / plus / dash / dot) so legend entries match
931
+ the data points. Bar / column / pie keep the square swatch.
932
+ - b1073ff: feat(site/playground): right / left chart legend stack centers
933
+ vertically. Previously the `r` and `l` legend positions both
934
+ stacked from a fixed `f.y + 12` top, the same as `tr`. PowerPoint
935
+ vertically-centers right / left legends inside the chart area; the
936
+ renderer now matches by computing `yStart` from the legend's total
937
+ height. `tr` keeps the top-anchored stack.
938
+ - ee27024: feat: chart legend honors authored `<c:txPr>` font / color.
939
+ `ChartSpec.legend.textStyle` carries the same `ChartTextStyle` shape
940
+ used for the chart title and axis titles. The playground renderer
941
+ projects font-size, bold, italic, and fill color onto every legend
942
+ label across all four position layouts (right / left / top / bottom /
943
+ top-right).
944
+ - fca13ca: feat(site/playground): line and area charts paint per-point value labels
945
+ when `<c:dLbls><c:showVal val="1"/>` is set. Labels sit just above each
946
+ marker and route through the chart number-format projector (so
947
+ `<c:numFmt formatCode="0%"/>` etc. apply the same as on bar / pie).
948
+ Honors the per-series → chart-level cascade.
949
+ - 263bf52: feat: `getParagraphLineSpacing(shape, p)` returns the paragraph's
950
+ `<a:lnSpc>` as `{ kind: 'pct' | 'pts', value }`. Percent values come
951
+ through as a unit fraction (1.5 = 150%); point values are pt.
952
+
953
+ The playground projects both forms to CSS `line-height` per paragraph,
954
+ and uses the existing `getParagraphSpacing` to project spcBef / spcAft
955
+ to `margin-top` / `margin-bottom`. Text blocks now keep the vertical
956
+ rhythm the deck authored instead of falling back to a fixed line
957
+ height for everything.
958
+
959
+ - 5381e9d: feat(chart): line / area charts now overlay the per-series
960
+ `<c:trendline>` when authored. Same regression types as the
961
+ column-chart variant (linear / log / exp / movingAvg / poly+power
962
+ fallback). Only emitted on the clustered layout — stacked plots
963
+ already convey the cumulative shape.
964
+ - ef4d410: feat: `getSlideMasterBackground(pres, layout)` returns the master's
965
+ `<p:bg>` (both `<p:bgPr>` and `<p:bgRef>` forms). Playground extends
966
+ its background fallback chain to slide → layout → master so brand
967
+ backgrounds authored on the master alone finally render on inheriting
968
+ slides instead of falling through to the theme's `light1`.
969
+ - 9c7a852: feat: `getSlideMasterBackgroundGradientFill(pres, layout)` returns
970
+ the master's gradient background when `<p:bg><p:bgPr><a:gradFill>`
971
+ is authored. Completes the three-level bg cascade for gradient
972
+ backgrounds — slides can fall through slide → layout → master.
973
+ - 60df186: feat: more name-based finders now accept `RegExp` —
974
+ `findSlideLayout(pres, name)`,
975
+ `findCommentAuthorByName(pres, authorName)`, and
976
+ `findSlidesByLayoutName(pres, layoutName)`. Pairs the RegExp support
977
+ recently added to `findShapeByName` / `findShapesByName` /
978
+ `findCommentsByAuthor` / `findSlideByTitle`. String callers unchanged.
979
+ - 10a9d05: feat: `getParagraphPropertiesEffective(pres, shape, p)` — paragraph-property
980
+ cascade mirroring the rPr one. Resolves alignment, left / right / first-line
981
+ indents, line spacing, paragraph spacing (before / after), and rtl through
982
+ the paragraph → text-body lstStyle → layout placeholder lstStyle →
983
+ master placeholder lstStyle → master txStyles chain.
984
+
985
+ The playground uses it as the primary source of paragraph properties so
986
+ placeholders inherit their default alignment / line-spacing / indent from
987
+ the layout / master, with any per-slide override winning on top.
988
+
989
+ Adds the `ParagraphProperties` type to the public surface.
990
+
991
+ - 263bf52: feat: `getShapeParagraphElements(shape, paragraphIndex)` returns the
992
+ inline children of a paragraph (runs, field placeholders, and line
993
+ breaks) in document order. Renderers can walk this list to reproduce
994
+ the full visible content — footer / date / slide-number `<a:fld>`
995
+ text was previously dropped by the strict `<a:r>`-only run accessors.
996
+
997
+ The playground now uses it: footer text + slide numbers + datetime
998
+ fields show up in the preview, and `<a:br>` line breaks render as
999
+ real `<br/>` inside the foreignObject body.
1000
+
1001
+ Adds the `ShapeParagraphElement` discriminated union to the public
1002
+ type surface.
1003
+
1004
+ - 263bf52: feat: `getParagraphIndent(shape, p)` returns the paragraph's
1005
+ `<a:pPr marL marR indent>` values in EMU (`null` for sides the
1006
+ paragraph doesn't author). Playground projects each side to CSS
1007
+ `padding-left` / `padding-right` / `text-indent` and skips the
1008
+ level-based default when the paragraph carries an explicit `marL`.
1009
+ - 263bf52: feat: `getShapePatternFill(pres, shape)` returns the pattern preset
1010
+ token plus the foreground / background colors resolved against the
1011
+ deck's theme. Pairs with the existing `setShapePatternFill`. The
1012
+ playground renderer now paints real SVG `<pattern>` tiles for the
1013
+ common `ST_PresetPatternVal` tokens (pct5..pct90, light/dark diagonal
1014
+ and horizontal/vertical stripes, grids, weave, wave, sphere, diamonds)
1015
+ instead of substituting a flat tint.
1016
+ - 1b08908: feat(chart): per-series `<c:ser><c:dLbls>` overrides. `ChartSeries.dataLabels`
1017
+ mirrors the chart-level `ChartSpec.dataLabels`; the series-level
1018
+ override wins when present. Playground's bar / column renderers
1019
+ check the per-series flag first so one series can show labels while
1020
+ others stay clean — common in financial decks.
1021
+ - 263bf52: feat: playground now applies the picture corrections that already
1022
+ shipped on the API: source-rectangle crop (`<a:srcRect>`), brightness
1023
+ (`<a:lumOff>`), contrast (`<a:lumMod>`), and opacity (`<a:alphaModFix>`).
1024
+
1025
+ Crops project to an enlarged `<image>` element clipped to the shape's
1026
+ bounds (matching PowerPoint's "Crop" tool). Brightness + contrast
1027
+ compose into an SVG `<feComponentTransfer>` filter. Opacity drives
1028
+ the `opacity` attribute directly.
1029
+
1030
+ - 7303fa8: feat(chart): `ChartSpec.firstSliceAngleDeg` reads `<c:firstSliceAng>`
1031
+ and `ChartSpec.holeSizePct` reads `<c:holeSize>` for doughnut charts.
1032
+ Playground rotates the first slice's starting position clockwise from
1033
+ 12 o'clock per the authored angle, and sizes the doughnut hole at the
1034
+ authored percent (10..90) of the outer radius instead of the
1035
+ hard-coded 55%.
1036
+ - 0ca34e1: feat: pie / doughnut slice explosion. `ChartSeries.pointExplosions`
1037
+ exposes the per-data-point pull-out percentage from `<c:dPt><c:explosion val="N"/>`,
1038
+ and the playground renderer offsets exploded slices (and their labels)
1039
+ outward along the slice mid-angle. Matches the "pulled-out" pie look
1040
+ authors get from Excel's "Vary colors by point" toggle.
1041
+ - 2e9776d: feat(site/playground): chart / media count badges on each slide.
1042
+ `getSlideCharts(slide)` and `getSlideMediaPartNames(pres, slide)`
1043
+ power two new badges (`N chart`, `N media`) showing how many chart
1044
+ shapes and how many media parts (images / audio / video) the slide
1045
+ references — useful for quick deck audits.
1046
+ - 997d507: feat(site/playground): comment badge tooltip carries the comment
1047
+ texts. The `N cmt` badge's `title=` attribute now joins each
1048
+ comment's body text so hovering surfaces the review remarks
1049
+ without opening PowerPoint.
1050
+ - 3ede588: feat(site/playground): additional slide badges — `hidden` (when
1051
+ `show="0"`) and `N cmt` (count of authored review comments). Threads
1052
+ `isSlideHidden` and `getSlideComments` through the slide-snapshot
1053
+ and surfaces both next to the existing `trans` / `anim` badges, so
1054
+ audit views see the full set of slide-level flags at a glance.
1055
+ - 7a489fa: feat(site/playground): layout-type badge tooltip carries the
1056
+ user-visible layout name. Hovering the small `obj` / `title` / etc.
1057
+ badge now reveals `layout: <Name> (type: <token>)` from
1058
+ `getSlideLayoutName(layout)`. Helps identify which authored layout
1059
+ each slide is bound to without leaving the playground.
1060
+ - d7d8571: feat(site/playground): show the slide's layout type (`title`, `obj`,
1061
+ `twoObj`, `blank`, …) as a badge next to the slide title. Reads
1062
+ `<p:sldLayout type="…">` via `getSlideLayout` + `getSlideLayoutType`
1063
+ so deck audits can spot which layout each slide is bound to without
1064
+ opening PowerPoint.
1065
+ - 5861d1e: feat(site/playground): include slide-master count in the
1066
+ "masters · layouts · sections" meta cell. `getPresentationSummary`
1067
+ already returned layout / section counts; the playground now also
1068
+ calls `getSlideMasterCount` so multi-master decks surface that fact
1069
+ in the audit panel.
1070
+ - 9a64bb4: feat(site/playground): expose `getPresentationSummary` data in the
1071
+ meta panel — theme name, layout / section counts, total shape count,
1072
+ and deck-wide flags (hidden slides, charts, comments, animations).
1073
+ Gives deck audits a one-glance overview without scrolling through
1074
+ every slide.
1075
+ - d9ba44d: feat(site/playground): render section dividers in the slide list.
1076
+ Reads `getSlideSections(pres)`, maps each section's first slide to
1077
+ the section's name, and renders a dashed divider above that slide
1078
+ in the slide list. Deck audits can now see the section grouping at
1079
+ a glance.
1080
+ - 62069f7: feat(site/playground): make the per-slide number an anchor link.
1081
+ Each slide's two-digit index in the head row is now an `<a
1082
+ href="#slide-N">` link, so users can right-click → "Copy link
1083
+ address" to share a deep link to a specific slide. Paired with the
1084
+ `id="slide-N"` already on each `<li>`, the link also scrolls the
1085
+ slide into view when clicked.
1086
+ - ffec23d: feat(site/playground): show speaker notes under each slide. The
1087
+ playground now calls `getSlideNotes` for every slide and renders a
1088
+ collapsible `<details>` block when notes exist, so users can
1089
+ inspect the deck author's notes without opening PowerPoint.
1090
+ - b90f1bc: feat(site/playground): show `validatePresentation` results. The
1091
+ playground now runs the validator after parsing and surfaces any
1092
+ issues in a dedicated panel (with severity tint and the offending
1093
+ part name when available). Lets users spot missing rels, dangling
1094
+ slide IDs, etc. without dropping into the test harness.
1095
+ - e078498: feat: `getShapeRunClickAction(shape, p, r)` returns the per-run
1096
+ hlinkClick action with the same `ShapeClickAction` discriminated
1097
+ union the shape-level `getShapeClickAction` uses. Recognises external
1098
+ URLs, slide-jump (`ppaction://hlinksldjump`), and the four
1099
+ nav-preset actions (next / prev / first / last slide). Lets callers
1100
+ treat per-run hyperlinks symmetrically with shape-level ones.
1101
+ - 5015413: feat(site/playground): per-run hyperlinks. Runs carrying `<a:hlinkClick>`
1102
+ now render in the theme's hyperlink color with an underline, and the
1103
+ span is wrapped in an `<a href>` so the preview is clickable. Per-run
1104
+ font / color / formatting overrides still apply on top — the link
1105
+ styling fills the gaps the run didn't author.
1106
+ - cc592d2: feat(site/playground): per-run slide-jump click actions render as
1107
+ in-page anchors. Mirrors the shape-level slide-jump support shipped
1108
+ in the prior batch — `getShapeRunClickAction` resolves to either a
1109
+ URL or `#slide-N` anchor, and the run-level `<a href>` wrapper
1110
+ respects whether the href is in-page (no `target=_blank`) or
1111
+ external.
1112
+ - 2207ed1: feat: scatter, radar, and bubble charts are now modeled as their own
1113
+ `ChartKind`s instead of being folded into `line`. `ChartSeries` gains
1114
+ `xValues` (`<c:xVal>`) and `bubbleSizes` (`<c:bubbleSize>`); `ChartSpec`
1115
+ gains `scatterStyle`, `radarStyle`, `bubbleScale`, and
1116
+ `bubbleSizeRepresents`. Read + render only: the preview draws real
1117
+ scatter (two value axes + markers), radar (polar spokes/rings), and
1118
+ bubble (area-proportional circles) plots, and the write path now rejects
1119
+ these kinds loudly — previously a read-modify-write silently corrupted a
1120
+ scatter chart into a line chart.
1121
+ - 08dc68b: feat(chart): `ChartSeries.lineWidthEmu` and `lineDash` read
1122
+ `<c:ser><c:spPr><a:ln>` per-series stroke width and preset dash.
1123
+ Playground line / area renderer uses the authored stroke width
1124
+ (scaled to px) and projects the preset dash to the same
1125
+ `stroke-dasharray` cadence shape strokes use.
1126
+ - e09e734: feat(chart): per-series marker symbol + size.
1127
+ `ChartSeries.markerSymbol` / `markerSizePt` read `<c:ser><c:marker>`
1128
+ (`<c:symbol val="…"/>` + `<c:size val="N"/>`). Playground line / area
1129
+ renderer emits the matching SVG glyph at each data point — circle /
1130
+ square / diamond / triangle / star / x / plus / dash / dot — sized
1131
+ per the authored point value. `none` hides the markers.
1132
+ - 995825f: feat: `setShapeHyperlink` and `setShapeRunHyperlink` now accept an
1133
+ optional `tooltip` argument that writes a `tooltip="…"` attribute on the
1134
+ emitted `<a:hlinkClick>`. Backwards compatible — calls that omit the new
1135
+ arg behave exactly as before.
1136
+
1137
+ fix: `getShapeHyperlinkTooltip` previously only looked at the shape's
1138
+ `<p:cNvPr><a:hlinkClick>`, missing the run-level tooltip that
1139
+ `setShapeHyperlink` writes. It now scans run-level `<a:rPr>` first
1140
+ (mirroring `getShapeHyperlink`'s read path) and falls back to the
1141
+ shape-click hyperlink — so the reader / writer pair is consistent.
1142
+
1143
+ - 051f4bb: feat: writers for the three stroke attributes that had readers but no
1144
+ setters — `setShapeStrokeCap(shape, 'rnd' | 'sq' | 'flat' | null)`,
1145
+ `setShapeStrokeJoin(shape, 'round' | 'bevel' | 'miter' | null)`, and
1146
+ `setShapeStrokeCompound(shape, 'sng' | 'dbl' | 'thickThin' | 'thinThick' | 'tri' | null)`.
1147
+
1148
+ Cap and compound map to `<a:ln cap=…/>` and `<a:ln cmpd=…/>` attributes;
1149
+ join writes one of the `<a:round/>` / `<a:bevel/>` / `<a:miter/>` child
1150
+ variants. Passing `null` clears the attribute / removes the child so the
1151
+ shape inherits the default. Creates `<a:ln>` if absent.
1152
+
1153
+ - 56da3ee: feat: `setShapeTextBodyRotationDeg(shape, rotationDeg | null)` — companion
1154
+ writer for the existing `getShapeTextBodyRotationDeg` reader. Sets
1155
+ `<a:bodyPr rot="N"/>` (in 60000ths of a degree, per OOXML) so the text
1156
+ body can rotate independently of the shape's own `<p:xfrm rot>`. Passing
1157
+ `null` or `0` clears the attribute so the shape inherits the default.
1158
+ - a65c05c: feat: `setShapeTextColumns(shape, { count, gapEmu? } | null)` — multi-
1159
+ column writer pairing the existing `getShapeTextColumns` reader. Writes
1160
+ `<a:bodyPr numCol="N" [spcCol="EMU"]/>`. Passing `null` clears both
1161
+ attributes so the text body falls back to PowerPoint's default single
1162
+ column. `count` must be `>= 2` (single column is the default — pass
1163
+ `null` instead); the function throws otherwise.
1164
+ - fea7725: feat: `setShapeTextDirection(shape, direction | null)` — companion
1165
+ writer for the existing `getShapeTextDirection` reader. Sets
1166
+ `<a:bodyPr vert="…"/>` with any of the six `ST_TextVerticalType`
1167
+ values (`vert`, `vert270`, `wordArtVert`, `eaVert`, `mongolianVert`,
1168
+ `wordArtVertRtl`); passing `null` or `'horz'` clears the attribute so
1169
+ the shape uses the default horizontal direction.
1170
+ - 4006813: feat: `setTableCellAnchor(cell, 'top' | 'center' | 'bottom' | null)` and
1171
+ `setTableCellMargins(cell, {left?, right?, top?, bottom?} | null)` —
1172
+ writers for two `<a:tcPr>` properties that already had readers
1173
+ (`getTableCellAnchor`, `getTableCellMargins`). The anchor setter maps
1174
+ `top`/`center`/`bottom` to the schema's `t`/`ctr`/`b` values and clears
1175
+ the attribute on `null`. The margins setter writes per-side EMU on
1176
+ `marL`/`marR`/`marT`/`marB`; sides set to `null`/`undefined` are
1177
+ stripped (PowerPoint falls back to its defaults); passing the whole
1178
+ arg as `null` clears every side. Both create `<a:tcPr>` if absent.
1179
+ - 3921802: feat: `setTableCellBorders(cell, sides | null)` — partial-update writer
1180
+ for all 6 cell-border slots (`left`, `right`, `top`, `bottom` + the
1181
+ `tlToBr` / `blToTr` diagonals). Pairs the existing
1182
+ `getTableCellBorders` reader. Sides listed with `null` are removed from
1183
+ `<a:tcPr>`; sides omitted are left untouched. Passing `null` as the
1184
+ whole `sides` arg clears every side. Creates `<a:tcPr>` if absent.
1185
+
1186
+ The diagonals are independent of the four cardinal sides — a
1187
+ strikethrough cell can have only `tlToBr`.
1188
+
1189
+ - c3e01b3: feat: `setTableCellTextDirection(cell, direction | null)` — vertical-
1190
+ text writer for table cells, paired with the existing
1191
+ `getTableCellTextDirection` reader. Same six `ST_TextVerticalType`
1192
+ values as `setShapeTextDirection`. Passing `null` (or `'horz'`) clears
1193
+ the `<a:tcPr vert="…"/>` attribute so the cell uses the default
1194
+ horizontal direction. Creates `<a:tcPr>` if absent.
1195
+ - 328f207: feat: `setTableStyleFlags(table, flags)` — partial-update writer for
1196
+ the six `<a:tblPr>` boolean style flags (`firstRow`, `lastRow`,
1197
+ `firstCol`, `lastCol`, `bandRow`, `bandCol`). Pairs the existing
1198
+ `getTableStyleFlags` reader. Only the keys present in `flags` are
1199
+ touched — omitted keys keep their current state. A flag set to `false`
1200
+ strips the attribute (matching how PowerPoint round-trips defaults).
1201
+ Creates `<a:tblPr>` if absent. Throws when the shape isn't a table
1202
+ graphic frame.
1203
+ - 1ea509b: feat: `setTableStyleId(table, styleId | null)` — writer for
1204
+ `<a:tbl><a:tblPr><a:tableStyleId>`. Pairs the existing `getTableStyleId`
1205
+ reader. Pass the curly-braced GUID (e.g.
1206
+ `'{5C22544A-7EE6-4342-B048-85BDC9FD1C3A}'` for PowerPoint's "Medium
1207
+ Style 2 - Accent 1") or `null` to remove the reference so the table
1208
+ uses the slide's default style. Creates `<a:tblPr>` if absent. Throws
1209
+ when the shape isn't a table graphic frame.
1210
+ - 2438696: feat(site/playground): shape `aria-label` from authored alt text.
1211
+ Each rendered shape with a non-empty alt title (or, as fallback,
1212
+ alt description) now exposes `role="img" aria-label="…"` on the
1213
+ root `<g>`. Screen readers announce decks the same way PowerPoint's
1214
+ Accessibility Inspector reports them, without affecting visuals.
1215
+ - b8e24d6: feat(site/playground): each shape's authored name surfaces as a
1216
+ `data-pptx-shape-name` attribute on its root `<g>` element. Lets
1217
+ DevTools, a11y inspectors, or test selectors target shapes by their
1218
+ PowerPoint name without parsing SVG geometry. Cheap to emit and has
1219
+ no visual impact.
1220
+ - fdd4770: feat: `getShapeTextBodyRotationDeg(shape)` returns the shape's text-body
1221
+ rotation from `<a:bodyPr rot="N"/>` (where N is in 60000ths of a
1222
+ degree). Distinct from the shape's geometry rotation (`<p:xfrm rot>`):
1223
+ this rotates the text body _inside_ the shape without rotating the
1224
+ geometry. The playground renderer pivots the text body around the
1225
+ inset midpoint when the angle is non-zero, matching PowerPoint's
1226
+ behaviour for vertical-label callouts and rotated text frames.
1227
+ - 263bf52: feat: `getSlideBackgroundGradientFill(slide)` returns the gradient
1228
+ stops + path for slides with a `<p:bgPr><a:gradFill>` background.
1229
+ Playground paints gradient slide backgrounds via the same projector
1230
+ that handles shape gradients (linear / radial / rect / shape).
1231
+ - 263bf52: feat: `getSlideBackgroundPatternFill(pres, slide)` returns the pattern
1232
+ preset + theme-resolved foreground / background for slides whose
1233
+ `<p:bgPr>` carries a `<a:pattFill>`. Playground now paints pattern
1234
+ slide backgrounds via the same SVG `<pattern>` tile generator that
1235
+ handles shape pattern fills.
1236
+ - c2bcc39: feat: `getSlideBackground` now handles `<p:bgRef>` (the theme-fill-
1237
+ reference variant of slide background, e.g. `<p:bgRef idx="1003">
1238
+ <a:schemeClr val="bg1"/></p:bgRef>`). Returns the inner color element
1239
+ as a solid fill so renderers paint the slide background even when
1240
+ the deck uses the theme-reference form instead of explicit `<p:bgPr>
1241
+ <a:solidFill>`.
1242
+ - 8b7cab6: feat: `slideHasAnimations(slide)` — per-slide animation predicate.
1243
+ Returns `true` when the slide carries a `<p:timing>` block (at least
1244
+ one authored animation effect). Complements the deck-wide
1245
+ `getPresentationSummary().hasAnimations`. The site playground uses
1246
+ it (plus `getSlideTransition`) to show small `anim` / `trans`
1247
+ badges next to each slide title so deck audits don't need to open
1248
+ PowerPoint.
1249
+ - c0e0dc2: feat(site/playground): shapes with slide-jump click actions
1250
+ (`<a:hlinkClick action="ppaction://hlinksldjump"/>`) render as
1251
+ in-page hash anchors. The renderer resolves the target via
1252
+ `getShapeClickAction` and emits `<a href="#slide-N">`; each slide's
1253
+ `<li>` carries `id="slide-N"` so clicks scroll to the target slide.
1254
+ Plain URL click actions render the same way as shape-level
1255
+ hyperlinks (with `target="_blank"`).
1256
+ - 7410df9: feat: `getSlideMasterPartName(slide)` returns the part-name of the
1257
+ slide master the slide inherits from. Useful for multi-master decks
1258
+ where different slides live under different brand templates and the
1259
+ caller needs to scope theme / fontScheme / clrMap lookups to the
1260
+ correct master.
1261
+ - 1f536ab: feat: `getShapeStrokeEffective(pres, shape)` walks the layout → master
1262
+ placeholder cascade when the shape's own stroke is `'inherit'`. Same
1263
+ discriminant types (solid / none / inherit) as `getShapeStroke`; first
1264
+ non-inherit layer wins. Playground uses it so placeholder outlines
1265
+ authored on the master / layout finally render.
1266
+ - 263bf52: feat: full stroke read-back surface — `getShapeStrokeCap`,
1267
+ `getShapeStrokeJoin`, `getShapeStrokeCompound` plus the existing
1268
+ `getShapeStrokeDash` / `getShapeStrokeArrow`. Renderers now have
1269
+ enough information to reproduce dashed outlines, rounded vs square
1270
+ caps, miter vs bevel joins, and per-end arrow heads.
1271
+
1272
+ The playground composes `stroke-dasharray` from the preset dash
1273
+ patterns (cadence multiplied by stroke width as PowerPoint does),
1274
+ emits SVG `<marker>` defs for triangle / stealth / diamond / oval
1275
+ arrowheads on connectors and shapes, and maps cap / join through.
1276
+
1277
+ - f68bd96: feat(site/playground): table cell borders honor `<a:prstDash>`. The
1278
+ reader already surfaced the dash token; the renderer now projects it
1279
+ to an SVG `stroke-dasharray` (scaled by the border's authored width).
1280
+ Applies to every side, the top-left → bottom-right diagonal, and the
1281
+ bottom-left → top-right diagonal.
1282
+ - a037fa7: feat: `getTableCellAnchor(cell)` returns the cell's vertical text
1283
+ anchor (`<a:tcPr anchor="t|ctr|b"/>`) as `'top' | 'center' |
1284
+ 'bottom' | null`. Playground projects each onto a CSS
1285
+ `justify-content` so cell text sits at the authored vertical
1286
+ position instead of always centering.
1287
+ - e50f1d6: feat(site/playground): table cell text honors authored `<a:tcPr
1288
+ marL/marR/marT/marB>` insets. The renderer previously hard-coded a
1289
+ 4-pixel pad on every side; it now converts each EMU-valued margin to
1290
+ px (falling back to 4px only when the side isn't authored) so cells
1291
+ with custom inner padding line up the way PowerPoint shows them.
1292
+ - 42cf575: feat: `getTableCellMargins(cell)` returns the cell's `<a:tcPr marL
1293
+ marR marT marB>` inset margins in EMU. Each side is `null` when the
1294
+ cell doesn't author it, so renderers know to fall back to
1295
+ PowerPoint's defaults (91440 EMU / 0.1 in horizontal, 45720 EMU /
1296
+ 0.05 in vertical).
1297
+ - ba94f5e: feat: `getTableCellParagraphs(cell)` returns a table cell's text as structured
1298
+ paragraphs — each carrying its alignment and per-run format (`size`, `bold`,
1299
+ `italic`, `color`, `font`, …) — the rich counterpart to `getTableCellText`,
1300
+ which only returns the flat visible string.
1301
+ - f05aa62: feat: `getTableCellTextDirection(cell)` reads `<a:tcPr vert="…"/>` —
1302
+ the same token set as `getShapeTextDirection` (`vert`, `vert270`,
1303
+ `eaVert`, `mongolianVert`, `wordArtVert`, `wordArtVertRtl`).
1304
+ Vertical column headers in tables commonly use `vert270` / `eaVert`
1305
+ so the header label reads bottom-to-top alongside its column.
1306
+ - 263bf52: feat: table span + border read-back.
1307
+
1308
+ - `getTableCellSpan(cell)` returns `{ gridSpan, rowSpan, hMerge, vMerge }`
1309
+ so renderers know which cells own a merged region and which are
1310
+ absorbed into one.
1311
+ - `getTableCellBorders(pres, cell)` returns per-side borders (left,
1312
+ right, top, bottom, plus the two diagonals tlToBr / blToTr) with
1313
+ theme-resolved colors, widths, and dash style.
1314
+
1315
+ Playground table rendering now honours both: merged cells are skipped
1316
+ on their absorbed positions, and per-cell borders render at the
1317
+ authored color / width on top of the default thin grid.
1318
+
1319
+ - 263bf52: feat: `getTableStyleFlags(table)` returns the `<a:tblPr>` boolean
1320
+ toggles — `firstRow` / `lastRow` / `firstCol` / `lastCol` / `bandRow`
1321
+ / `bandCol`. Playground projects each onto a theme-derived tint
1322
+ (accent1 for header / footer rows, 92%-white-mixed accent for bands)
1323
+ when the cell doesn't supply an explicit fill of its own. Header text
1324
+ rendered on the accent gets white text instead of the default body
1325
+ color, matching PowerPoint's built-in table styles.
1326
+ - 243e731: feat: `getTableStyleId(table)` returns the GUID string inside
1327
+ `<a:tbl><a:tblPr><a:tableStyleId>`. PowerPoint references built-in
1328
+ table styles (`{5C22544A-…}` = Medium Style 2 - Accent 1, etc.) and
1329
+ theme-local styles by GUID. Returns `null` when the table doesn't
1330
+ author one.
1331
+ - dfae64a: feat: add `getSlideLayoutShapes(pres, layout)` and `getSlideMasterShapes(pres,
1332
+ layout)` — the non-placeholder decorative shapes (corner bars, divider lines,
1333
+ logos, watermark text) on a slide layout and its master, as render-ready
1334
+ `SlideShapeData`. Unlike the older flat `getSlideLayoutBackgroundShapes`, these
1335
+ include pictures and groups and work with every `getShape*` reader, so a
1336
+ picture logo's bytes resolve (against the layout/master's own relationship
1337
+ table). For reading/rendering — the handles are bound to the layout/master
1338
+ part, not a slide.
1339
+ - 263bf52: feat: `getShapeTextColumns(shape)` returns `{ count, gapEmu? }` for
1340
+ text bodies that author `<a:bodyPr numCol="N" spcCol="EMU"/>`.
1341
+ Playground emits `column-count` / `column-gap` on the foreignObject,
1342
+ so newspaper-style multi-column placeholders flow correctly.
1343
+ - 263bf52: feat: extend `TextFormat` with the remaining commonly-authored
1344
+ `CT_TextCharacterProperties` (ECMA-376 §17.18.83) attributes:
1345
+
1346
+ - `strike` — `true` / `false` / `'sngStrike'` / `'dblStrike'`
1347
+ - `spc` — character spacing in 1/100 pt
1348
+ - `kern` — kerning threshold in half-points
1349
+ - `baseline` — superscript / subscript offset as a unit fraction
1350
+ - `cap` — `'none'` / `'small'` / `'all'`
1351
+ - `highlight` — per-run background color
1352
+
1353
+ All round-trip through `setShapeRunFormat` / `getShapeRunFormat` and
1354
+ flow through `getShapeRunFormatEffective`'s inheritance cascade. The
1355
+ playground renderer honours each of them in the rendered HTML.
1356
+
1357
+ - dc98eb1: feat(site/playground): default text body to the theme's font scheme.
1358
+ `<a:fontScheme><a:majorFont>` becomes the default face for title /
1359
+ ctrTitle placeholders; `<a:minorFont>` covers everything else. The
1360
+ existing per-run `<a:rPr typeface>` override still wins. Templates
1361
+ that brand-themselves to Aptos / Inter / etc. now render with their
1362
+ authored fonts instead of always falling back to Calibri.
1363
+ - 263bf52: feat: Tier B fidelity batch.
1364
+
1365
+ - `getShapeTextDirection(shape)` returns the `<a:bodyPr vert="…"/>`
1366
+ token (`vert`, `vert270`, `wordArtVert`, `eaVert`, `mongolianVert`,
1367
+ `wordArtVertRtl`). Playground projects each onto a CSS
1368
+ `writing-mode` / `text-orientation` declaration so Asian and
1369
+ Mongolian-style vertical text renders without manual transforms.
1370
+ - Playground wraps shapes carrying a `<a:hlinkClick>` in an SVG `<a>`
1371
+ element so the preview is clickable — matches PowerPoint's
1372
+ slide-show behaviour for shape-level hyperlinks.
1373
+ - Group shape rendering now applies the group's own `<a:xfrm rot
1374
+ flipH flipV>` to the whole subtree before the scale + translate
1375
+ that maps internal coords onto slide coords.
1376
+
1377
+ - 4688af3: feat: chart trendline `<c:forward>` / `<c:backward>` extensions.
1378
+ `ChartTrendline.forward` and `backward` carry the N-period
1379
+ extrapolation past the last / before the first data point. The
1380
+ playground renderer projects the linear fit further along the x-axis
1381
+ by `N * step` so projected-future trendlines render the way
1382
+ PowerPoint shows them. Moving-average / log / poly trendlines keep
1383
+ their data-range output since extrapolation isn't meaningful for
1384
+ them.
1385
+ - 57117a7: feat: chart value-axis tick labels honor `<c:valAx><c:txPr><a:bodyPr
1386
+ rot="N"/>`. `ChartSpec.valueAxisLabelRotationDeg` returns the rotation
1387
+ in degrees (converted from OOXML's 60000ths-of-a-degree). The
1388
+ playground renders each value-axis tick label with a
1389
+ `transform=rotate()` around its anchor, symmetric to the
1390
+ `categoryAxisLabelRotationDeg` we already projected.
1391
+ - 3e1c8a1: feat: chart value-axis exposes `<c:scaling><c:logBase val="N"/>`.
1392
+ `ChartAxisScaling.logBase` carries the authored log base (commonly
1393
+ `2`, `10`, or `Math.E`). The reader clamps to PowerPoint's `[2, 1000]`
1394
+ range. Callers that round-trip charts now preserve the log-scale
1395
+ flag; the playground renderer still draws linear (log-scale
1396
+ projection is a follow-up — exposing the field unblocks it).
1397
+
1398
+ ### Patch Changes
1399
+
1400
+ - 499c590: fix: presentation handles now interoperate across the `pptx-kit` and
1401
+ `pptx-kit/node` entry points. The two entries ship as separate bundles,
1402
+ and the opaque handles (`PresentationData`, `SlideData`, …) were keyed by
1403
+ plain `Symbol`s minted per bundle. Loading a deck with
1404
+ `loadPresentationFile` (from `pptx-kit/node`) and then reading it with,
1405
+ say, `getSlides` (from `pptx-kit`) crashed with
1406
+ `Cannot read properties of undefined`. The handle keys now use the global
1407
+ symbol registry (`Symbol.for`), so a handle from either entry is readable
1408
+ by the other — and by companion packages that bundle their own reader copy.
1409
+ - 8fc8f12: fix(site): playground stopped rendering after `SlideCommentData` was made
1410
+ opaque and `getSlideMediaPartNames` lost its `(pres, slide)` two-arg
1411
+ form. The playground was still doing `comment.text` and
1412
+ `getSlideMediaPartNames(pres, slide)`, both of which threw at runtime.
1413
+ Switched to the public `getCommentText(comment)` accessor and the
1414
+ single-arg `getSlideMediaPartNames(slide)` signature.
1415
+ - 3fb5101: fix: `getShapeRunFormatEffective` / `getParagraphPropertiesEffective` no longer
1416
+ inherit the slide master's `bodyStyle` for plain text boxes. A shape without a
1417
+ `<p:ph>` is not a placeholder, so its unsized runs now resolve to no inherited
1418
+ size (consumers apply the ~18pt text-box default) instead of wrongly picking up
1419
+ the master body size (often much larger). Real placeholders — including ones
1420
+ whose `<p:ph>` omits a `type` — still inherit as before. This makes effective
1421
+ text formatting match what PowerPoint and LibreOffice render for text boxes.
1422
+ - cfe8b69: fix: placeholder inheritance now applies the OOXML `ctrTitle`↔`title` and
1423
+ `subTitle`→`body` type equivalence. A `ctrTitle` (centered title) now inherits
1424
+ its layout/master `title` placeholder's `bodyPr` (e.g. `anchor="ctr"`),
1425
+ `lstStyle`, and geometry instead of dropping them — fixing
1426
+ `getShapeBodyPrEffective`, `getShapeBoundsResolved`,
1427
+ `getShapeRunFormatEffective`, and `getParagraphPropertiesEffective` for
1428
+ centered titles and subtitles.
1429
+ - 610ecac: fix(validator): `validatePresentation` now flags duplicate
1430
+ `<p:cNvPr id="N">` values inside a single slide's `<p:spTree>` as
1431
+ errors. PowerPoint requires every shape's non-visual ID to be unique
1432
+ within its slide; duplicates often appear after pasting shapes from
1433
+ another slide without re-allocating IDs. The walk recurses into
1434
+ `<p:grpSp>` so duplicates nested in groups are also caught.
1435
+
1436
+ ## 1.0.0
1437
+
1438
+ ### Major Changes
1439
+
1440
+ - f47b78b: **1.0.0** — first stable release. The public API is now frozen under SemVer.
1441
+
1442
+ **What works at 1.0:**
1443
+
1444
+ - **Read** any `.pptx` produced by PowerPoint, Keynote, Google Slides, or
1445
+ LibreOffice Impress, and save it back without corruption. Unknown
1446
+ extensions are preserved verbatim on round-trip.
1447
+ - **Template editing**: token / text replace across slides and speaker
1448
+ notes, image swap with geometry preserved, slide CRUD with placeholder
1449
+ inheritance from layout / master.
1450
+ - **Authoring on top of an existing master**: 180+ preset shapes, custom
1451
+ text formatting, tables, embedded charts (column / line / bar / pie /
1452
+ doughnut / area) with auto-generated xlsx, solid / gradient / pattern /
1453
+ image fills, shadows and glows, rotation / flip / z-order, hyperlinks
1454
+ and click actions, notes and comments, slide transitions, simple
1455
+ entrance / exit animations.
1456
+ - **Diagnostics**: `validatePresentation` returns invariant violations;
1457
+ every XML part is validated against the ECMA-376 XSDs in CI.
1458
+ - **Bundling**: one ESM build runs in both Node ≥ 20 and modern browsers.
1459
+ Tree-shaking is enforced by a CI test — minimal `load → save` bundle
1460
+ is < 75 KB unminified, full fn-API bundle is ~120 KB.
1461
+
1462
+ **Deferred to post-1.0** (read pass-through preserved on round-trip):
1463
+
1464
+ - Constructing new themes / masters / layouts from scratch.
1465
+ - SmartArt authoring.
1466
+ - Complex animation timing-tree authoring.
1467
+ - OLE / ActiveX authoring.
1468
+ - Document encryption (read + write).
1469
+
1470
+ **Performance (M-series Node 20):** 100-slide synthetic deck saves in
1471
+ ~25 ms, loads in ~20 ms. 100 MB templates fit comfortably under the 2 s
1472
+ load/save targets.
1473
+
1474
+ **Migration:** if you were on the pre-1.0 class API
1475
+ (`Presentation` / `Slide` / `SlideShape` / `SlideLayout`), see the
1476
+ preceding changeset for the rename table. There is no class API at 1.0.
1477
+
1478
+ - 665c979: **BREAKING**: the class-based API (`Presentation`, `Slide`, `SlideShape`,
1479
+ `SlideLayout`) has been removed. Use the free-function API for every
1480
+ capability — one canonical path per operation.
1481
+
1482
+ | Was | Now |
1483
+ | -------------------------------- | ---------------------------------------- |
1484
+ | `Presentation.load(bytes)` | `loadPresentation(bytes)` |
1485
+ | `Presentation.create()` | `createPresentation()` |
1486
+ | `pres.save()` | `savePresentation(pres)` |
1487
+ | `pres.slides` | `getSlides(pres)` |
1488
+ | `pres.slideLayouts` | `getSlideLayouts(pres)` |
1489
+ | `pres.addSlide({ layout })` | `addSlide(pres, { layout })` |
1490
+ | `pres.removeSlide(slide)` | `removeSlide(pres, slide)` |
1491
+ | `pres.moveSlide(slide, i)` | `moveSlide(pres, slide, i)` |
1492
+ | `pres.duplicateSlide(slide)` | `duplicateSlide(pres, slide)` |
1493
+ | `pres.replaceTokens(map)` | `replaceTokensInPresentation(pres, map)` |
1494
+ | `slide.shapes` | `getSlideShapes(slide)` |
1495
+ | `slide.findPlaceholder('title')` | `findSlidePlaceholder(slide, 'title')` |
1496
+ | `slide.addTextBox(opts)` | `addSlideTextBox(slide, opts)` |
1497
+ | `slide.addShape(opts)` | `addSlideShape(slide, opts)` |
1498
+ | `slide.addImage(bytes, opts)` | `addSlideImage(slide, bytes, opts)` |
1499
+ | `slide.addTable(opts)` | `addSlideTable(slide, opts)` |
1500
+ | `slide.addLine(opts)` | `addSlideLine(slide, opts)` |
1501
+ | `slide.setBackground(color)` | `setSlideBackground(slide, color)` |
1502
+ | `slide.setTransition(opts)` | `setSlideTransition(slide, opts)` |
1503
+ | `slide.setNotes(text)` | `setSlideNotes(slide, text)` |
1504
+ | `slide.layout` | `getSlideLayout(slide)` |
1505
+ | `slide.notes` | `getSlideNotes(slide)` |
1506
+ | `slide.text` | `getSlideText(slide)` |
1507
+ | `shape.text` | `getShapeText(shape)` |
1508
+ | `shape.setText(value)` | `setShapeText(shape, value)` |
1509
+ | `shape.position` | `getShapePosition(shape)` |
1510
+ | `shape.setPosition(x, y)` | `setShapePosition(shape, x, y)` |
1511
+ | `shape.setFill(color)` | `setShapeFill(shape, color)` |
1512
+ | `shape.setStroke(opts)` | `setShapeStroke(shape, opts)` |
1513
+ | `shape.setRotation(deg)` | `setShapeRotation(shape, deg)` |
1514
+ | `shape.setHyperlink(url)` | `setShapeHyperlink(shape, url)` |
1515
+ | `layout.name` | `getSlideLayoutName(layout)` |
1516
+
1517
+ Node entry (`pptx-kit/node`) drops the `Presentation` subclass; use
1518
+ `loadPresentationFile` / `savePresentationToFile` instead.
1519
+
1520
+ **Why**: every capability used to have two paths through the public API
1521
+ — a class method and a free function. The duplication hurt
1522
+ discoverability (which one should you use?), made the bundle larger
1523
+ (class consumers dragged the whole prototype in), and forced every
1524
+ breaking change to land in two places. The free-function API is the
1525
+ canonical surface from now on.
1526
+
1527
+ ### Minor Changes
1528
+
1529
+ - b41c502: Comprehensive feature surface for PPTX authoring + editing. This is the
1530
+ first release that covers every L1–L4 capability in the foundation
1531
+ plan. Highlights:
1532
+
1533
+ **Round-trip + template editing (L1 / L2)**
1534
+
1535
+ - `loadPresentation` / `savePresentation` (`Uint8Array` / `ArrayBuffer` / `Blob`).
1536
+ - Node convenience: `loadPresentationFile`, `savePresentationToFile`.
1537
+ - Token replace: `replaceTokensInPresentation`, `replaceTokensInSlide`.
1538
+ - Free-text replace: `replaceTextInPresentation`, `replaceTextInSlide`.
1539
+ - Slide CRUD: `addSlide`, `removeSlide`, `moveSlide`, `duplicateSlide`,
1540
+ `getSlideAt`, `getSlideIndex`, `clearSlideShapes`, `sortSlides`.
1541
+ - Cross-deck: `importSlide` (with image-media propagation).
1542
+ - Cross-slide: `copyShape`.
1543
+ - Diagnostics: `validatePresentation`, `getPresentationSummary`,
1544
+ `listPackageParts`, `readPackagePart`, `getMediaParts`,
1545
+ `setMediaPartBytes`, `compactPackage`.
1546
+
1547
+ **Authoring (L3)**
1548
+
1549
+ - Shapes: `addSlideTextBox`, `addSlideShape` (180+ presets),
1550
+ `addSlideLine`, `addSlideTable`, `addSlideImage`, `addSlideChart`.
1551
+ - Charts: `bar` / `column` / `line` / `pie` / `doughnut` / `area` with
1552
+ embedded xlsx; read/update via `getSlideCharts` / `setChartSpec`.
1553
+ - Tables: per-cell access (`getTableCells`, `setTableCellText`,
1554
+ `setTableCellFill`, `setTableCellTextFormat`,
1555
+ `setTableCellAlignment`); row + column insert/remove.
1556
+ - Slide layout swap: `setSlideLayout`, `findSlideLayout`.
1557
+
1558
+ **Text**
1559
+
1560
+ - Per-shape: `setShapeText`, `setShapeBullets`, `setShapeAlignment`,
1561
+ `setShapeTextFormat`, `setShapeHyperlink`, `setShapeTextAnchor`,
1562
+ `setShapeTextMargins`, `setShapeTextWrap`, `setShapeTextAutoFit`.
1563
+ - Per-paragraph: `setParagraphAlignment`, `setParagraphBullet`,
1564
+ `setParagraphLevel`, `setParagraphSpacing` + read-back pairs.
1565
+ - Per-run: `setShapeRunFormat`, `setShapeRunText`,
1566
+ `getShapeRunFormat`, `getShapeParagraphCount`, `getShapeRunCount`,
1567
+ `getShapeRunText`.
1568
+
1569
+ **Geometry**
1570
+
1571
+ - Position / size / rotation / flip + combined `setShapeBounds` /
1572
+ `getShapeBounds`. Z-order: `bringShapeToFront`, `sendShapeToBack`,
1573
+ `bringShapeForward`, `sendShapeBackward`.
1574
+
1575
+ **Fill / stroke / effects**
1576
+
1577
+ - Fill kinds: solid, gradient, pattern, image, none + `getShapeFill`
1578
+ read-back.
1579
+ - Stroke: color + width + dash + arrowheads + `getShapeStroke` /
1580
+ `getShapeStrokeDash` / `getShapeStrokeArrow` read-back.
1581
+ - Effects: `setShapeShadow`, `setShapeGlow`, `clearShapeEffects` +
1582
+ `getShapeEffect` read-back.
1583
+
1584
+ **Pictures**
1585
+
1586
+ - Crop, opacity, brightness (`lumOff`), contrast (`lumMod`),
1587
+ image replacement, image-as-fill. Read-back pairs for every setter.
1588
+
1589
+ **Slide-level (L4)**
1590
+
1591
+ - Notes (`getSlideNotes` / `setSlideNotes`).
1592
+ - Transitions (every effect + read-back).
1593
+ - Animations (`fadeIn` / `fadeOut` / `appear` / `disappear`) +
1594
+ read-back.
1595
+ - Comments (legacy schema, author dedup, optional position + date).
1596
+ - Backgrounds: solid color or embedded picture; read-back.
1597
+ - Visibility: `setSlideHidden` / `isSlideHidden`.
1598
+ - Slide sections (p14:sectionLst).
1599
+ - Slide size + presets (`SLIDE_SIZE_4_3` / `16_9` / `16_10`).
1600
+ - Slide title shortcut (`getSlideTitle` / `setSlideTitle`).
1601
+ - Click actions: URL / slide jump / preset nav + read-back.
1602
+
1603
+ **Theme + package**
1604
+
1605
+ - `getPresentationTheme` — color scheme (`accent1`–`accent6`, `dark1`,
1606
+ `light1`, `hyperlink`, ...).
1607
+ - `getMediaParts`, `listPackageParts`, `readPackagePart` for audit /
1608
+ export workflows.
1609
+
1610
+ **Tree-shake**
1611
+
1612
+ - The minimal `load`+`save` import is ~60 KB; the full fn-API
1613
+ bundle ~123 KB. CI guard via `test/tree-shake.test.ts`.
1614
+
1615
+ All emitted XML validates against the ECMA-376 strict schemas
1616
+ (pml.xsd, dml-chart.xsd, opc-relationships.xsd, opc-contentTypes.xsd)
1617
+ via Layer-1 tests.
1618
+
1619
+ **Additional helpers** (all tree-shakeable free functions)
1620
+
1621
+ - Properties: `getCoreProperties` / `setCoreProperties`,
1622
+ `getExtendedProperties` / `setExtendedProperties`, plus convenience
1623
+ `getPresentationCreated`, `getPresentationModified`,
1624
+ `incrementRevision`, `touchModified`.
1625
+ - Thumbnail: `getThumbnail` / `setThumbnail` / `removeThumbnail`.
1626
+ - Theme: `getPresentationTheme`, `getPresentationFonts`.
1627
+ - Slide queries: `getSlideCount`, `getSlideLayoutCount`,
1628
+ `getVisibleSlides`, `getHiddenSlides`, `getSlidesWithNotes`,
1629
+ `getSlidesWithComments`, `getSlidesWithImages`,
1630
+ `getSlidesWithCharts`, `getSlidesWithTables`,
1631
+ `getSlidesByLayout`, `findSlideByTitle`, `findSlideByText`,
1632
+ `findSlidesByText`, `findSlideByPartName`,
1633
+ `findSlideLayoutByType`, `findSlideLayoutByPartName`.
1634
+ - Bulk inventories: `getAllNotes`, `getAllComments`, `getAllCharts`,
1635
+ `getAllTables`, `getAllImages`, `getPresentationText`,
1636
+ `getSlideOutline`.
1637
+ - Shape introspection: `getShapeAt`, `getShapeIndex`,
1638
+ `getShapeSlide`, `getShapeXmlString`, `getShapeChartKind`,
1639
+ `getShapeChartSpec`, `getShapeImageFillBytes`,
1640
+ `getShapeImageFormat`, `getShapeImagePartName`,
1641
+ `getShapeAltTitle` / `setShapeAltTitle`,
1642
+ `getShapeDescription` / `setShapeDescription`.
1643
+ - Shape predicates: `isChartShape`, `isTableShape`,
1644
+ `isShapeHidden` / `setShapeHidden`, `isShapePlaceholder`,
1645
+ `hasShapeImage`, `hasShapeText`.
1646
+ - Shape search: `findShapeByText`, `findShapesByText`,
1647
+ `findShapesByKind`, `findChartByKind`,
1648
+ `findChartsBySeriesName`, `findCommentsByAuthor`,
1649
+ `findSlidePlaceholders`, `findSlidePlaceholderByIdx`.
1650
+ - Mutation: `setShapeRunHyperlink`, `getShapeRunHyperlink`,
1651
+ `getSlideBody`, `appendShapeText`,
1652
+ `appendSlideNotes`, `removeSlideNotes`,
1653
+ `swapSlides`, `mergePresentations`, `slidesUsingMediaPart`,
1654
+ `setTableColumnWidth`, `setTableRowHeight`, `getTableColumnWidths`,
1655
+ `getTableRowHeights`, `getTableCellAlignment`, `getTableCellFill`.
1656
+ - Diagnostics: `getSlideXmlString`, `getSlidePartName`,
1657
+ `getSlideLayoutPartName`, `getSlidesByLayout`.