@beyondwork/docx-react-component 1.0.47 → 1.0.49
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -11
- package/package.json +30 -41
- package/src/api/public-types.ts +199 -13
- package/src/compare/diff-engine.ts +4 -0
- package/src/core/commands/add-scope.ts +257 -0
- package/src/core/commands/formatting-commands.ts +2 -0
- package/src/core/commands/index.ts +9 -1
- package/src/core/commands/text-commands.ts +3 -1
- package/src/core/schema/text-schema.ts +95 -1
- package/src/core/selection/anchor-conversion.ts +112 -0
- package/src/core/selection/review-anchors.ts +108 -3
- package/src/core/state/text-transaction.ts +103 -7
- package/src/internal/harness-debug-ports.ts +168 -0
- package/src/io/chart-preview-resolver.ts +59 -1
- package/src/io/docx-session.ts +226 -38
- package/src/io/export/serialize-main-document.ts +46 -0
- package/src/io/export/serialize-paragraph-formatting.ts +8 -0
- package/src/io/export/serialize-run-formatting.ts +10 -1
- package/src/io/export/serialize-settings.ts +421 -0
- package/src/io/export/serialize-styles.ts +10 -0
- package/src/io/normalize/normalize-text.ts +1 -0
- package/src/io/ooxml/chart/chart-style-table.ts +543 -0
- package/src/io/ooxml/chart/color-palette.ts +101 -0
- package/src/io/ooxml/chart/compose-series-color.ts +147 -0
- package/src/io/ooxml/chart/parse-axis.ts +277 -0
- package/src/io/ooxml/chart/parse-chart-space.ts +885 -0
- package/src/io/ooxml/chart/parse-series.ts +635 -0
- package/src/io/ooxml/chart/resolve-color.ts +261 -0
- package/src/io/ooxml/chart/types.ts +439 -0
- package/src/io/ooxml/parse-block-structure.ts +99 -0
- package/src/io/ooxml/parse-complex-content.ts +90 -2
- package/src/io/ooxml/parse-main-document.ts +156 -1
- package/src/io/ooxml/parse-paragraph-formatting.ts +46 -0
- package/src/io/ooxml/parse-run-formatting.ts +49 -0
- package/src/io/ooxml/parse-scope-markers.ts +184 -0
- package/src/io/ooxml/parse-settings-blueprint.ts +349 -0
- package/src/io/ooxml/parse-settings.ts +97 -1
- package/src/io/ooxml/parse-styles.ts +65 -0
- package/src/io/ooxml/parse-theme.ts +2 -127
- package/src/io/ooxml/property-grab-bag.ts +211 -0
- package/src/io/ooxml/xml-attr-helpers.ts +59 -1
- package/src/io/ooxml/xml-parser.ts +142 -0
- package/src/model/canonical-document.ts +160 -0
- package/src/model/scope-markers.ts +144 -0
- package/src/runtime/collab/base-doc-fingerprint.ts +99 -0
- package/src/runtime/collab/checkpoint-election.ts +75 -0
- package/src/runtime/collab/checkpoint-scheduler.ts +204 -0
- package/src/runtime/collab/checkpoint-store.ts +115 -0
- package/src/runtime/collab/event-types.ts +27 -0
- package/src/runtime/collab/index.ts +29 -0
- package/src/runtime/collab/remote-cursor-awareness.ts +167 -0
- package/src/runtime/collab/runtime-collab-sync.ts +330 -0
- package/src/runtime/collab/workflow-shared.ts +247 -0
- package/src/runtime/document-locations.ts +1 -9
- package/src/runtime/document-outline.ts +1 -9
- package/src/runtime/document-runtime.ts +288 -65
- package/src/runtime/editor-surface/capabilities.ts +63 -50
- package/src/runtime/hyperlink-color-resolver.ts +119 -0
- package/src/runtime/layout/layout-engine-version.ts +8 -1
- package/src/runtime/prerender/cache-envelope.ts +19 -7
- package/src/runtime/prerender/cache-key.ts +25 -14
- package/src/runtime/prerender/canonical-document-hash.ts +63 -0
- package/src/runtime/prerender/customxml-cache.ts +211 -0
- package/src/runtime/prerender/customxml-probe.ts +78 -0
- package/src/runtime/prerender/prerender-document.ts +74 -7
- package/src/runtime/scope-resolver.ts +148 -0
- package/src/runtime/scope-tag-registry.ts +10 -0
- package/src/runtime/surface-projection.ts +102 -37
- package/src/runtime/theme-color-resolver.ts +188 -0
- package/src/runtime/workflow-markup.ts +7 -18
- package/src/ui/WordReviewEditor.tsx +48 -2
- package/src/ui/editor-runtime-boundary.ts +42 -1
- package/src/ui/headless/selection-helpers.ts +10 -23
- package/src/ui/runtime-shortcut-dispatch.ts +12 -7
- package/src/ui/unsupported-previews-policy.ts +23 -0
- package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +10 -0
- package/src/ui-tailwind/editor-surface/perf-probe.ts +1 -0
- package/src/ui-tailwind/page-stack/tw-page-stack-chrome-layer.tsx +47 -0
- package/src/ui-tailwind/page-stack/use-visible-block-range.ts +88 -0
- package/src/ui-tailwind/tw-review-workspace.tsx +16 -1
|
@@ -0,0 +1,635 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse `<c:ser>` elements and their numCache/strCache bodies.
|
|
3
|
+
*
|
|
4
|
+
* The sparse-point semantics are important: `<c:numCache>` carries a
|
|
5
|
+
* `c:ptCount` attribute and zero or more `<c:pt idx="N">` children; any
|
|
6
|
+
* index between 0 and ptCount that does not appear represents a blank
|
|
7
|
+
* (null) value. Consumers rely on that layout to render gaps / stacked
|
|
8
|
+
* zeros / "span" lines per the chart family's c:dispBlanksAs setting.
|
|
9
|
+
*
|
|
10
|
+
* Callers that already have the series element in hand should use
|
|
11
|
+
* `parseBarSeriesNode` to avoid re-parsing the XML.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
findChildOptional,
|
|
16
|
+
findFirstDescendant,
|
|
17
|
+
localName,
|
|
18
|
+
readFloatVal,
|
|
19
|
+
readIntVal,
|
|
20
|
+
readOnOff,
|
|
21
|
+
textContent,
|
|
22
|
+
} from "../xml-attr-helpers.ts";
|
|
23
|
+
import type { XmlElementNode } from "../xml-element.ts";
|
|
24
|
+
import { parseXml } from "../xml-parser.ts";
|
|
25
|
+
|
|
26
|
+
import type {
|
|
27
|
+
BubbleSeries,
|
|
28
|
+
ColorMod,
|
|
29
|
+
ColorRef,
|
|
30
|
+
DataPointOverride,
|
|
31
|
+
FillSpec,
|
|
32
|
+
LineSeries,
|
|
33
|
+
MarkerSpec,
|
|
34
|
+
PieSeries,
|
|
35
|
+
ScatterSeries,
|
|
36
|
+
Series,
|
|
37
|
+
ShapeProperties,
|
|
38
|
+
StrokeSpec,
|
|
39
|
+
} from "./types.ts";
|
|
40
|
+
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
// Series
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
|
|
45
|
+
/** Parse a `<c:ser>` XML fragment into a `Series`. */
|
|
46
|
+
export function parseBarSeries(serXml: string): Series {
|
|
47
|
+
const root = parseXml(serXml);
|
|
48
|
+
const node = findFirstDescendant(root, "ser");
|
|
49
|
+
if (!node) {
|
|
50
|
+
return emptySeries();
|
|
51
|
+
}
|
|
52
|
+
return parseBarSeriesNode(node);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Parse the `Series` fields that every chart family shares: idx, order,
|
|
57
|
+
* name (c:tx), categories (c:cat), values (c:val), and the explicit
|
|
58
|
+
* fill/stroke overrides from c:spPr.
|
|
59
|
+
*
|
|
60
|
+
* Family-specific parsers (line markers, pie explosion, scatter xValues,
|
|
61
|
+
* etc.) wrap this and attach additional fields to the base shape.
|
|
62
|
+
*/
|
|
63
|
+
export function parseSeriesBase(node: XmlElementNode): Series {
|
|
64
|
+
const idx = readIntVal(findChildOptional(node, "idx")) ?? 0;
|
|
65
|
+
const order = readIntVal(findChildOptional(node, "order")) ?? 0;
|
|
66
|
+
const name = readSeriesName(findChildOptional(node, "tx"));
|
|
67
|
+
const categories = extractStrCache(findChildOptional(node, "cat"));
|
|
68
|
+
const values = extractNumCache(findChildOptional(node, "val"));
|
|
69
|
+
const spPr = parseShapeProperties(findChildOptional(node, "spPr"));
|
|
70
|
+
const dataPoints = parseDataPointOverrides(node);
|
|
71
|
+
|
|
72
|
+
const series: Series = {
|
|
73
|
+
idx,
|
|
74
|
+
order,
|
|
75
|
+
categories,
|
|
76
|
+
values,
|
|
77
|
+
};
|
|
78
|
+
if (name !== undefined) series.name = name;
|
|
79
|
+
if (spPr) series.spPr = spPr;
|
|
80
|
+
if (dataPoints.length > 0) series.dataPoints = dataPoints;
|
|
81
|
+
return series;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Parse a pre-parsed `<c:ser>` element into a `Series`. Thin wrapper over
|
|
86
|
+
* `parseSeriesBase`, kept for call-site readability in bar/column parsing
|
|
87
|
+
* where "bar" makes the intent clear. Per-data-point overrides (c:dPt) —
|
|
88
|
+
* highlights, invertIfNegative, color overrides — are attached by the
|
|
89
|
+
* base parser so all category-oriented families (bar, area) surface them.
|
|
90
|
+
*/
|
|
91
|
+
export function parseBarSeriesNode(node: XmlElementNode): Series {
|
|
92
|
+
return parseSeriesBase(node);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function emptySeries(): Series {
|
|
96
|
+
return { idx: 0, order: 0, categories: [], values: [] };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ---------------------------------------------------------------------------
|
|
100
|
+
// Line series
|
|
101
|
+
// ---------------------------------------------------------------------------
|
|
102
|
+
|
|
103
|
+
/** Parse a `<c:ser>` XML fragment from a lineChart into a `LineSeries`. */
|
|
104
|
+
export function parseLineSeries(serXml: string): LineSeries {
|
|
105
|
+
const root = parseXml(serXml);
|
|
106
|
+
const node = findFirstDescendant(root, "ser");
|
|
107
|
+
if (!node) {
|
|
108
|
+
return emptySeries();
|
|
109
|
+
}
|
|
110
|
+
return parseLineSeriesNode(node);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Parse a pre-parsed `<c:ser>` element from a lineChart. Returns the common
|
|
115
|
+
* `Series` fields plus optional per-series smooth + marker overrides. Omits
|
|
116
|
+
* the optional keys entirely when absent so strict-type consumers can
|
|
117
|
+
* distinguish "not specified" from "explicitly false / none".
|
|
118
|
+
*/
|
|
119
|
+
export function parseLineSeriesNode(node: XmlElementNode): LineSeries {
|
|
120
|
+
const base = parseSeriesBase(node);
|
|
121
|
+
const smoothNode = findChildOptional(node, "smooth");
|
|
122
|
+
const smooth = readOnOff(smoothNode);
|
|
123
|
+
const marker = parseMarkerSpec(findChildOptional(node, "marker"));
|
|
124
|
+
|
|
125
|
+
const series: LineSeries = { ...base };
|
|
126
|
+
if (smooth !== undefined) series.smooth = smooth;
|
|
127
|
+
if (marker) series.marker = marker;
|
|
128
|
+
return series;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ---------------------------------------------------------------------------
|
|
132
|
+
// Marker
|
|
133
|
+
// ---------------------------------------------------------------------------
|
|
134
|
+
|
|
135
|
+
const MARKER_SYMBOLS: ReadonlySet<MarkerSpec["symbol"]> = new Set<
|
|
136
|
+
MarkerSpec["symbol"]
|
|
137
|
+
>([
|
|
138
|
+
"circle",
|
|
139
|
+
"square",
|
|
140
|
+
"diamond",
|
|
141
|
+
"triangle",
|
|
142
|
+
"x",
|
|
143
|
+
"star",
|
|
144
|
+
"dot",
|
|
145
|
+
"dash",
|
|
146
|
+
"plus",
|
|
147
|
+
"picture",
|
|
148
|
+
"none",
|
|
149
|
+
"auto",
|
|
150
|
+
]);
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Parse a `<c:marker>` element. Returns undefined when the node is absent.
|
|
154
|
+
* Returns a `MarkerSpec` with `symbol: "auto"` when the element is present
|
|
155
|
+
* but declares no symbol — OOXML's default for a marker with no subclass.
|
|
156
|
+
* Unknown symbol values also collapse to `"auto"` so renderers never see
|
|
157
|
+
* an unrecognised enum.
|
|
158
|
+
*/
|
|
159
|
+
export function parseMarkerSpec(
|
|
160
|
+
markerNode: XmlElementNode | undefined,
|
|
161
|
+
): MarkerSpec | undefined {
|
|
162
|
+
if (!markerNode) return undefined;
|
|
163
|
+
const symbolRaw = findChildOptional(markerNode, "symbol")?.attributes["val"];
|
|
164
|
+
const symbol: MarkerSpec["symbol"] =
|
|
165
|
+
symbolRaw && isMarkerSymbol(symbolRaw) ? symbolRaw : "auto";
|
|
166
|
+
const size = readIntVal(findChildOptional(markerNode, "size"));
|
|
167
|
+
const spPr = parseShapeProperties(findChildOptional(markerNode, "spPr"));
|
|
168
|
+
|
|
169
|
+
const marker: MarkerSpec = { symbol };
|
|
170
|
+
if (size !== undefined) marker.size = size;
|
|
171
|
+
if (spPr) marker.spPr = spPr;
|
|
172
|
+
return marker;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function isMarkerSymbol(value: string): value is MarkerSpec["symbol"] {
|
|
176
|
+
return MARKER_SYMBOLS.has(value as MarkerSpec["symbol"]);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// ---------------------------------------------------------------------------
|
|
180
|
+
// Pie series
|
|
181
|
+
// ---------------------------------------------------------------------------
|
|
182
|
+
|
|
183
|
+
/** Parse a `<c:ser>` XML fragment from a pieChart/doughnutChart into `PieSeries`. */
|
|
184
|
+
export function parsePieSeries(serXml: string): PieSeries {
|
|
185
|
+
const root = parseXml(serXml);
|
|
186
|
+
const node = findFirstDescendant(root, "ser");
|
|
187
|
+
if (!node) {
|
|
188
|
+
return { idx: 0, order: 0, values: [] };
|
|
189
|
+
}
|
|
190
|
+
return parsePieSeriesNode(node);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Parse a pre-parsed `<c:ser>` element from a pieChart/doughnutChart.
|
|
195
|
+
*
|
|
196
|
+
* PieSeries extends SeriesBase (idx/order/name/spPr) with:
|
|
197
|
+
* - values: the numeric cache from c:val/c:numRef/c:numCache (sparse, nulls preserved).
|
|
198
|
+
* - explosion: series-level default offset percent from c:explosion val.
|
|
199
|
+
* - dataPoints: optional per-slice overrides (c:dPt[idx]/{spPr,explosion,bubble3D}).
|
|
200
|
+
*
|
|
201
|
+
* Categories for pie live on `PieChartModel.categoryLabels`, not on the
|
|
202
|
+
* series — the group parser reads them from the first series' c:cat.
|
|
203
|
+
*/
|
|
204
|
+
export function parsePieSeriesNode(node: XmlElementNode): PieSeries {
|
|
205
|
+
const idx = readIntVal(findChildOptional(node, "idx")) ?? 0;
|
|
206
|
+
const order = readIntVal(findChildOptional(node, "order")) ?? 0;
|
|
207
|
+
const name = readSeriesName(findChildOptional(node, "tx"));
|
|
208
|
+
const values = extractNumCache(findChildOptional(node, "val"));
|
|
209
|
+
const spPr = parseShapeProperties(findChildOptional(node, "spPr"));
|
|
210
|
+
const explosion = readIntVal(findChildOptional(node, "explosion"));
|
|
211
|
+
const dataPoints = parseDataPointOverrides(node);
|
|
212
|
+
|
|
213
|
+
const series: PieSeries = { idx, order, values };
|
|
214
|
+
if (name !== undefined) series.name = name;
|
|
215
|
+
if (spPr) series.spPr = spPr;
|
|
216
|
+
if (explosion !== undefined) series.explosion = explosion;
|
|
217
|
+
if (dataPoints.length > 0) series.dataPoints = dataPoints;
|
|
218
|
+
return series;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// ---------------------------------------------------------------------------
|
|
222
|
+
// Data-point overrides (c:dPt)
|
|
223
|
+
// ---------------------------------------------------------------------------
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Walk `<c:dPt>` children of a `<c:ser>` element and return typed overrides.
|
|
227
|
+
*
|
|
228
|
+
* Each override must have a `<c:idx val="N">` child; malformed entries (no
|
|
229
|
+
* idx) are skipped silently — the well-formed entries still apply. Optional
|
|
230
|
+
* children captured: spPr (fill/stroke override), explosion (pie slice
|
|
231
|
+
* offset percent), bubble3D (tolerated on non-bubble charts), marker (line
|
|
232
|
+
* variants), invertIfNegative (bar variants).
|
|
233
|
+
*/
|
|
234
|
+
// ---------------------------------------------------------------------------
|
|
235
|
+
// Scatter series
|
|
236
|
+
// ---------------------------------------------------------------------------
|
|
237
|
+
|
|
238
|
+
/** Parse a `<c:ser>` XML fragment from a scatterChart into `ScatterSeries`. */
|
|
239
|
+
export function parseScatterSeries(serXml: string): ScatterSeries {
|
|
240
|
+
const root = parseXml(serXml);
|
|
241
|
+
const node = findFirstDescendant(root, "ser");
|
|
242
|
+
if (!node) {
|
|
243
|
+
return { idx: 0, order: 0, xValues: [], yValues: [] };
|
|
244
|
+
}
|
|
245
|
+
return parseScatterSeriesNode(node);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Parse a pre-parsed `<c:ser>` element from a scatterChart.
|
|
250
|
+
*
|
|
251
|
+
* ScatterSeries extends SeriesBase (idx/order/name/spPr) with:
|
|
252
|
+
* - xValues, yValues: parallel numeric caches from c:xVal and c:yVal.
|
|
253
|
+
* - smooth: per-series override (c:smooth).
|
|
254
|
+
* - marker: optional MarkerSpec (c:marker).
|
|
255
|
+
*
|
|
256
|
+
* Categories don't apply to scatter — both axes are numeric.
|
|
257
|
+
*/
|
|
258
|
+
export function parseScatterSeriesNode(node: XmlElementNode): ScatterSeries {
|
|
259
|
+
const idx = readIntVal(findChildOptional(node, "idx")) ?? 0;
|
|
260
|
+
const order = readIntVal(findChildOptional(node, "order")) ?? 0;
|
|
261
|
+
const name = readSeriesName(findChildOptional(node, "tx"));
|
|
262
|
+
const spPr = parseShapeProperties(findChildOptional(node, "spPr"));
|
|
263
|
+
const xValues = extractNumCache(findChildOptional(node, "xVal"));
|
|
264
|
+
const yValues = extractNumCache(findChildOptional(node, "yVal"));
|
|
265
|
+
const smooth = readOnOff(findChildOptional(node, "smooth"));
|
|
266
|
+
const marker = parseMarkerSpec(findChildOptional(node, "marker"));
|
|
267
|
+
|
|
268
|
+
const series: ScatterSeries = { idx, order, xValues, yValues };
|
|
269
|
+
if (name !== undefined) series.name = name;
|
|
270
|
+
if (spPr) series.spPr = spPr;
|
|
271
|
+
if (smooth !== undefined) series.smooth = smooth;
|
|
272
|
+
if (marker) series.marker = marker;
|
|
273
|
+
return series;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// ---------------------------------------------------------------------------
|
|
277
|
+
// Bubble series
|
|
278
|
+
// ---------------------------------------------------------------------------
|
|
279
|
+
|
|
280
|
+
/** Parse a `<c:ser>` XML fragment from a bubbleChart into `BubbleSeries`. */
|
|
281
|
+
export function parseBubbleSeries(serXml: string): BubbleSeries {
|
|
282
|
+
const root = parseXml(serXml);
|
|
283
|
+
const node = findFirstDescendant(root, "ser");
|
|
284
|
+
if (!node) {
|
|
285
|
+
return { idx: 0, order: 0, xValues: [], yValues: [], sizes: [] };
|
|
286
|
+
}
|
|
287
|
+
return parseBubbleSeriesNode(node);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Parse a pre-parsed `<c:ser>` element from a bubbleChart.
|
|
292
|
+
*
|
|
293
|
+
* BubbleSeries extends SeriesBase with x/y caches plus a third
|
|
294
|
+
* `sizes` channel from c:bubbleSize. Each triple (x, y, size) is one
|
|
295
|
+
* bubble; all three arrays are parallel-indexed, nulls preserved.
|
|
296
|
+
*/
|
|
297
|
+
export function parseBubbleSeriesNode(node: XmlElementNode): BubbleSeries {
|
|
298
|
+
const idx = readIntVal(findChildOptional(node, "idx")) ?? 0;
|
|
299
|
+
const order = readIntVal(findChildOptional(node, "order")) ?? 0;
|
|
300
|
+
const name = readSeriesName(findChildOptional(node, "tx"));
|
|
301
|
+
const spPr = parseShapeProperties(findChildOptional(node, "spPr"));
|
|
302
|
+
const xValues = extractNumCache(findChildOptional(node, "xVal"));
|
|
303
|
+
const yValues = extractNumCache(findChildOptional(node, "yVal"));
|
|
304
|
+
const sizes = extractNumCache(findChildOptional(node, "bubbleSize"));
|
|
305
|
+
|
|
306
|
+
const series: BubbleSeries = { idx, order, xValues, yValues, sizes };
|
|
307
|
+
if (name !== undefined) series.name = name;
|
|
308
|
+
if (spPr) series.spPr = spPr;
|
|
309
|
+
return series;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
export function parseDataPointOverrides(
|
|
313
|
+
serNode: XmlElementNode,
|
|
314
|
+
): DataPointOverride[] {
|
|
315
|
+
const overrides: DataPointOverride[] = [];
|
|
316
|
+
for (const child of serNode.children) {
|
|
317
|
+
if (child.type !== "element" || localName(child.name) !== "dPt") continue;
|
|
318
|
+
const idxNode = findChildOptional(child, "idx");
|
|
319
|
+
const idx = readIntVal(idxNode);
|
|
320
|
+
if (idx === undefined) continue;
|
|
321
|
+
|
|
322
|
+
const override: DataPointOverride = { idx };
|
|
323
|
+
const spPr = parseShapeProperties(findChildOptional(child, "spPr"));
|
|
324
|
+
if (spPr) override.spPr = spPr;
|
|
325
|
+
const marker = parseMarkerSpec(findChildOptional(child, "marker"));
|
|
326
|
+
if (marker) override.marker = marker;
|
|
327
|
+
const explosion = readIntVal(findChildOptional(child, "explosion"));
|
|
328
|
+
if (explosion !== undefined) override.explosion = explosion;
|
|
329
|
+
const invert = readOnOff(findChildOptional(child, "invertIfNegative"));
|
|
330
|
+
if (invert !== undefined) override.invertIfNegative = invert;
|
|
331
|
+
const bubble3D = readOnOff(findChildOptional(child, "bubble3D"));
|
|
332
|
+
if (bubble3D !== undefined) override.bubble3D = bubble3D;
|
|
333
|
+
|
|
334
|
+
overrides.push(override);
|
|
335
|
+
}
|
|
336
|
+
return overrides;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function readSeriesName(txNode: XmlElementNode | undefined): string | undefined {
|
|
340
|
+
if (!txNode) return undefined;
|
|
341
|
+
|
|
342
|
+
// Case 1: <c:tx><c:strRef><c:strCache>…<c:v>Name</c:v>…
|
|
343
|
+
const strRef = findChildOptional(txNode, "strRef");
|
|
344
|
+
if (strRef) {
|
|
345
|
+
const cache = findFirstDescendant(strRef, "strCache");
|
|
346
|
+
if (cache) {
|
|
347
|
+
for (const child of cache.children) {
|
|
348
|
+
if (child.type !== "element" || localName(child.name) !== "pt") continue;
|
|
349
|
+
const v = findChildOptional(child, "v");
|
|
350
|
+
if (v) return textContent(v);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
// strCache missing — fall back to any c:v descendant under the strRef.
|
|
354
|
+
const directV = findFirstDescendant(strRef, "v");
|
|
355
|
+
if (directV) return textContent(directV);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Case 2: <c:tx><c:v>Name</c:v> (inline literal).
|
|
359
|
+
const direct = findChildOptional(txNode, "v");
|
|
360
|
+
if (direct) return textContent(direct);
|
|
361
|
+
|
|
362
|
+
// Case 3: <c:tx><c:rich><a:p><a:r><a:t>Name</a:t>… — plain-text collapse.
|
|
363
|
+
const rich = findChildOptional(txNode, "rich");
|
|
364
|
+
if (rich) {
|
|
365
|
+
const firstT = findFirstDescendant(rich, "t");
|
|
366
|
+
if (firstT) return textContent(firstT);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
return undefined;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// ---------------------------------------------------------------------------
|
|
373
|
+
// Cache extraction
|
|
374
|
+
// ---------------------------------------------------------------------------
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Extract a sparse numeric cache. Returns an array of `ptCount` length
|
|
378
|
+
* filled with `null`, with each `<c:pt idx="N"><c:v>value</c:v></c:pt>`
|
|
379
|
+
* populating index N. Values that fail to parse as finite numbers become
|
|
380
|
+
* `null` as well.
|
|
381
|
+
*
|
|
382
|
+
* The `expected` parameter lets callers bump the array length when
|
|
383
|
+
* `c:ptCount` is absent (e.g. when the cache is a companion to another
|
|
384
|
+
* series whose length is known).
|
|
385
|
+
*/
|
|
386
|
+
export function extractNumCache(
|
|
387
|
+
container: XmlElementNode | undefined,
|
|
388
|
+
expected?: number,
|
|
389
|
+
): Array<number | null> {
|
|
390
|
+
if (!container) return [];
|
|
391
|
+
// Prefer numCache. The strCache fallback is intentional for numRef-
|
|
392
|
+
// backed category axes where Word emits the numeric series through the
|
|
393
|
+
// string-cache path (e.g. a Year axis authored as numbers but displayed
|
|
394
|
+
// as labels). Each stringified value is then parseFloat'd; non-finite
|
|
395
|
+
// strings collapse to null, preserving the sparse-index contract.
|
|
396
|
+
const cacheNode =
|
|
397
|
+
findFirstDescendant(container, "numCache") ??
|
|
398
|
+
findFirstDescendant(container, "strCache");
|
|
399
|
+
if (!cacheNode) return [];
|
|
400
|
+
const ptCount = readIntVal(findChildOptional(cacheNode, "ptCount")) ?? 0;
|
|
401
|
+
const length = Math.max(ptCount, expected ?? 0);
|
|
402
|
+
const out: Array<number | null> = new Array(length).fill(null);
|
|
403
|
+
for (const child of cacheNode.children) {
|
|
404
|
+
if (child.type !== "element" || localName(child.name) !== "pt") continue;
|
|
405
|
+
const idxRaw = child.attributes["idx"];
|
|
406
|
+
const idx = idxRaw ? Number.parseInt(idxRaw, 10) : Number.NaN;
|
|
407
|
+
if (!Number.isFinite(idx) || idx < 0 || idx >= out.length) continue;
|
|
408
|
+
const vNode = findChildOptional(child, "v");
|
|
409
|
+
if (!vNode) continue;
|
|
410
|
+
const n = Number.parseFloat(textContent(vNode));
|
|
411
|
+
if (Number.isFinite(n)) out[idx] = n;
|
|
412
|
+
}
|
|
413
|
+
return out;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Extract a sparse string cache. Mirrors `extractNumCache` but fills
|
|
418
|
+
* missing indices with empty strings. Falls back to `numCache` if the
|
|
419
|
+
* container only has numeric data (so a numRef-backed category axis still
|
|
420
|
+
* yields stringified labels).
|
|
421
|
+
*/
|
|
422
|
+
export function extractStrCache(
|
|
423
|
+
container: XmlElementNode | undefined,
|
|
424
|
+
): string[] {
|
|
425
|
+
if (!container) return [];
|
|
426
|
+
const cache =
|
|
427
|
+
findFirstDescendant(container, "strCache") ??
|
|
428
|
+
findFirstDescendant(container, "numCache");
|
|
429
|
+
if (!cache) return [];
|
|
430
|
+
const ptCount = readIntVal(findChildOptional(cache, "ptCount")) ?? 0;
|
|
431
|
+
const out: string[] = new Array(ptCount).fill("");
|
|
432
|
+
for (const child of cache.children) {
|
|
433
|
+
if (child.type !== "element" || localName(child.name) !== "pt") continue;
|
|
434
|
+
const idxRaw = child.attributes["idx"];
|
|
435
|
+
const idx = idxRaw ? Number.parseInt(idxRaw, 10) : Number.NaN;
|
|
436
|
+
if (!Number.isFinite(idx) || idx < 0 || idx >= out.length) continue;
|
|
437
|
+
const vNode = findChildOptional(child, "v");
|
|
438
|
+
if (!vNode) continue;
|
|
439
|
+
out[idx] = textContent(vNode);
|
|
440
|
+
}
|
|
441
|
+
return out;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// ---------------------------------------------------------------------------
|
|
445
|
+
// Shape properties (spPr) — fills and strokes
|
|
446
|
+
// ---------------------------------------------------------------------------
|
|
447
|
+
|
|
448
|
+
export function parseShapeProperties(
|
|
449
|
+
spPrNode: XmlElementNode | undefined,
|
|
450
|
+
): ShapeProperties | undefined {
|
|
451
|
+
if (!spPrNode) return undefined;
|
|
452
|
+
const fill = parseFill(spPrNode);
|
|
453
|
+
const stroke = parseStroke(findChildOptional(spPrNode, "ln"));
|
|
454
|
+
if (!fill && !stroke) return undefined;
|
|
455
|
+
const result: ShapeProperties = {};
|
|
456
|
+
if (fill) result.fill = fill;
|
|
457
|
+
if (stroke) result.stroke = stroke;
|
|
458
|
+
return result;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function parseFill(spPrNode: XmlElementNode): FillSpec | undefined {
|
|
462
|
+
// Explicit fill-none sentinel.
|
|
463
|
+
if (findChildOptional(spPrNode, "noFill")) {
|
|
464
|
+
return { kind: "none" };
|
|
465
|
+
}
|
|
466
|
+
const solid = findChildOptional(spPrNode, "solidFill");
|
|
467
|
+
if (solid) {
|
|
468
|
+
const color = parseColorRef(solid);
|
|
469
|
+
if (color) return { kind: "solid", color };
|
|
470
|
+
}
|
|
471
|
+
// Gradient fill: extract c:gsLst stops so renderers can honor the
|
|
472
|
+
// authored gradient or, as a minimum, fall back to the first stop as a
|
|
473
|
+
// solid color. Stage 4 renderers consume the full stop list.
|
|
474
|
+
const gradNode = findChildOptional(spPrNode, "gradFill");
|
|
475
|
+
if (gradNode) {
|
|
476
|
+
const gradient = parseGradientFill(gradNode);
|
|
477
|
+
if (gradient) return gradient;
|
|
478
|
+
}
|
|
479
|
+
// Pattern fill: Stage 1 degrades to the foreground color as a solid
|
|
480
|
+
// fallback so bars stay visible. A richer pattern renderer is deferred.
|
|
481
|
+
const pattNode = findChildOptional(spPrNode, "pattFill");
|
|
482
|
+
if (pattNode) {
|
|
483
|
+
const fg = findChildOptional(pattNode, "fgClr");
|
|
484
|
+
if (fg) {
|
|
485
|
+
const color = parseColorRef(fg);
|
|
486
|
+
if (color) return { kind: "solid", color };
|
|
487
|
+
}
|
|
488
|
+
return { kind: "none" };
|
|
489
|
+
}
|
|
490
|
+
return undefined;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* Parse a DrawingML `a:gradFill` element per ECMA-376 §20.1.8.33.
|
|
495
|
+
*
|
|
496
|
+
* Shape: `<a:gradFill><a:gsLst><a:gs pos="N"><a:srgbClr|a:schemeClr…/></a:gs>…</a:gsLst><a:lin ang="…"/></a:gradFill>`.
|
|
497
|
+
* Stops' `pos` attribute is in parts-per-100,000 (0..100000); we normalize
|
|
498
|
+
* to 0..1 so renderers can project onto their own coordinate system.
|
|
499
|
+
*
|
|
500
|
+
* Returns undefined when the gradient has no usable stops so the caller
|
|
501
|
+
* can fall through to the fill chain's subsequent branches. Mal-authored
|
|
502
|
+
* gradients (no gsLst, no color children) degrade gracefully to `none`
|
|
503
|
+
* rather than producing an invalid FillSpec.
|
|
504
|
+
*/
|
|
505
|
+
function parseGradientFill(gradNode: XmlElementNode): FillSpec | undefined {
|
|
506
|
+
const gsLst = findChildOptional(gradNode, "gsLst");
|
|
507
|
+
if (!gsLst) return { kind: "none" };
|
|
508
|
+
const stops: Array<{ pos: number; color: ColorRef }> = [];
|
|
509
|
+
for (const child of gsLst.children) {
|
|
510
|
+
if (child.type !== "element" || localName(child.name) !== "gs") continue;
|
|
511
|
+
const posRaw = child.attributes["pos"];
|
|
512
|
+
const posPpm = posRaw ? Number.parseInt(posRaw, 10) : Number.NaN;
|
|
513
|
+
const color = parseColorRef(child);
|
|
514
|
+
if (!color) continue;
|
|
515
|
+
const pos = Number.isFinite(posPpm)
|
|
516
|
+
? Math.max(0, Math.min(1, posPpm / 100_000))
|
|
517
|
+
: stops.length / 2;
|
|
518
|
+
stops.push({ pos, color });
|
|
519
|
+
}
|
|
520
|
+
if (stops.length === 0) return { kind: "none" };
|
|
521
|
+
|
|
522
|
+
// a:lin carries angle in 1/60,000 of a degree; a:path carries a path
|
|
523
|
+
// type (linear/radial/rect) but no angle. Stage 1 only reads linear.
|
|
524
|
+
let angle: number | undefined;
|
|
525
|
+
const lin = findChildOptional(gradNode, "lin");
|
|
526
|
+
if (lin) {
|
|
527
|
+
const angRaw = lin.attributes["ang"];
|
|
528
|
+
if (angRaw) {
|
|
529
|
+
const n = Number.parseInt(angRaw, 10);
|
|
530
|
+
if (Number.isFinite(n)) angle = n / 60_000;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
const gradient: FillSpec = { kind: "gradient", stops };
|
|
535
|
+
if (angle !== undefined) gradient.angle = angle;
|
|
536
|
+
return gradient;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
function parseStroke(lnNode: XmlElementNode | undefined): StrokeSpec | undefined {
|
|
540
|
+
if (!lnNode) return undefined;
|
|
541
|
+
const stroke: StrokeSpec = {};
|
|
542
|
+
const widthAttr = lnNode.attributes["w"];
|
|
543
|
+
if (widthAttr) {
|
|
544
|
+
const w = Number.parseInt(widthAttr, 10);
|
|
545
|
+
if (Number.isFinite(w)) stroke.widthEmu = w;
|
|
546
|
+
}
|
|
547
|
+
if (findChildOptional(lnNode, "noFill")) {
|
|
548
|
+
stroke.noFill = true;
|
|
549
|
+
}
|
|
550
|
+
const solid = findChildOptional(lnNode, "solidFill");
|
|
551
|
+
if (solid) {
|
|
552
|
+
const color = parseColorRef(solid);
|
|
553
|
+
if (color) stroke.color = color;
|
|
554
|
+
}
|
|
555
|
+
const prstDash = findChildOptional(lnNode, "prstDash");
|
|
556
|
+
if (prstDash) {
|
|
557
|
+
const dashVal = prstDash.attributes["val"];
|
|
558
|
+
switch (dashVal) {
|
|
559
|
+
case "solid":
|
|
560
|
+
case "dash":
|
|
561
|
+
case "dashDot":
|
|
562
|
+
case "lgDash":
|
|
563
|
+
case "lgDashDot":
|
|
564
|
+
case "sysDash":
|
|
565
|
+
case "sysDashDot":
|
|
566
|
+
stroke.dash = dashVal;
|
|
567
|
+
break;
|
|
568
|
+
default:
|
|
569
|
+
break;
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
if (
|
|
573
|
+
stroke.widthEmu === undefined &&
|
|
574
|
+
stroke.noFill === undefined &&
|
|
575
|
+
stroke.color === undefined &&
|
|
576
|
+
stroke.dash === undefined
|
|
577
|
+
) {
|
|
578
|
+
return undefined;
|
|
579
|
+
}
|
|
580
|
+
return stroke;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/**
|
|
584
|
+
* Parse a color reference child out of a fill wrapper (solidFill, stroke
|
|
585
|
+
* solidFill, etc.). Supports srgbClr and schemeClr with the full OOXML
|
|
586
|
+
* modifier set (lumMod, lumOff, shade, tint, satMod, hueMod, alpha).
|
|
587
|
+
*/
|
|
588
|
+
function parseColorRef(wrapper: XmlElementNode): ColorRef | undefined {
|
|
589
|
+
for (const child of wrapper.children) {
|
|
590
|
+
if (child.type !== "element") continue;
|
|
591
|
+
const local = localName(child.name);
|
|
592
|
+
if (local === "srgbClr") {
|
|
593
|
+
const val = child.attributes["val"];
|
|
594
|
+
if (!val) continue;
|
|
595
|
+
return { kind: "srgb", value: `#${val.toUpperCase()}` };
|
|
596
|
+
}
|
|
597
|
+
if (local === "schemeClr") {
|
|
598
|
+
const val = child.attributes["val"];
|
|
599
|
+
if (!val) continue;
|
|
600
|
+
const mods = parseColorMods(child);
|
|
601
|
+
const ref: ColorRef = { kind: "scheme", value: val };
|
|
602
|
+
if (mods.length > 0) ref.mods = mods;
|
|
603
|
+
return ref;
|
|
604
|
+
}
|
|
605
|
+
if (local === "sysClr") {
|
|
606
|
+
// Take the resolved lastClr value; treat as sRGB to stay in-band.
|
|
607
|
+
const lastClr = child.attributes["lastClr"];
|
|
608
|
+
if (lastClr) return { kind: "srgb", value: `#${lastClr.toUpperCase()}` };
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
return undefined;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
function parseColorMods(colorNode: XmlElementNode): ColorMod[] {
|
|
615
|
+
const mods: ColorMod[] = [];
|
|
616
|
+
for (const child of colorNode.children) {
|
|
617
|
+
if (child.type !== "element") continue;
|
|
618
|
+
const local = localName(child.name);
|
|
619
|
+
if (
|
|
620
|
+
local !== "lumMod" &&
|
|
621
|
+
local !== "lumOff" &&
|
|
622
|
+
local !== "shade" &&
|
|
623
|
+
local !== "tint" &&
|
|
624
|
+
local !== "satMod" &&
|
|
625
|
+
local !== "hueMod" &&
|
|
626
|
+
local !== "alpha"
|
|
627
|
+
) {
|
|
628
|
+
continue;
|
|
629
|
+
}
|
|
630
|
+
const value = readFloatVal(child);
|
|
631
|
+
if (value === undefined) continue;
|
|
632
|
+
mods.push({ kind: local, value });
|
|
633
|
+
}
|
|
634
|
+
return mods;
|
|
635
|
+
}
|