@beyondwork/docx-react-component 1.0.47 → 1.0.48
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/package.json +1 -1
- package/src/api/public-types.ts +115 -1
- 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/schema/text-schema.ts +95 -1
- package/src/core/state/text-transaction.ts +17 -5
- package/src/io/chart-preview-resolver.ts +27 -0
- package/src/io/docx-session.ts +226 -38
- package/src/io/export/serialize-main-document.ts +37 -0
- 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/parse-axis.ts +277 -0
- package/src/io/ooxml/chart/parse-chart-space.ts +813 -0
- package/src/io/ooxml/chart/parse-series.ts +570 -0
- package/src/io/ooxml/chart/resolve-color.ts +251 -0
- package/src/io/ooxml/chart/types.ts +420 -0
- package/src/io/ooxml/parse-block-structure.ts +99 -0
- package/src/io/ooxml/parse-complex-content.ts +87 -2
- package/src/io/ooxml/parse-main-document.ts +115 -1
- 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/xml-attr-helpers.ts +59 -1
- package/src/io/ooxml/xml-parser.ts +142 -0
- package/src/model/canonical-document.ts +94 -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 +22 -0
- package/src/runtime/collab/remote-cursor-awareness.ts +167 -0
- package/src/runtime/collab/runtime-collab-sync.ts +279 -0
- package/src/runtime/document-runtime.ts +214 -16
- package/src/runtime/editor-surface/capabilities.ts +63 -50
- 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 +8 -1
- package/src/ui/WordReviewEditor.tsx +30 -0
- package/src/ui/editor-runtime-boundary.ts +6 -1
- package/src/ui/runtime-shortcut-dispatch.ts +12 -7
|
@@ -0,0 +1,570 @@
|
|
|
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
|
+
|
|
71
|
+
const series: Series = {
|
|
72
|
+
idx,
|
|
73
|
+
order,
|
|
74
|
+
categories,
|
|
75
|
+
values,
|
|
76
|
+
};
|
|
77
|
+
if (name !== undefined) series.name = name;
|
|
78
|
+
if (spPr) series.spPr = spPr;
|
|
79
|
+
return series;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Parse a pre-parsed `<c:ser>` element into a `Series`. Thin wrapper over
|
|
84
|
+
* `parseSeriesBase`, kept for call-site readability in bar/column parsing
|
|
85
|
+
* where "bar" makes the intent clear.
|
|
86
|
+
*/
|
|
87
|
+
export function parseBarSeriesNode(node: XmlElementNode): Series {
|
|
88
|
+
return parseSeriesBase(node);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function emptySeries(): Series {
|
|
92
|
+
return { idx: 0, order: 0, categories: [], values: [] };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ---------------------------------------------------------------------------
|
|
96
|
+
// Line series
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
98
|
+
|
|
99
|
+
/** Parse a `<c:ser>` XML fragment from a lineChart into a `LineSeries`. */
|
|
100
|
+
export function parseLineSeries(serXml: string): LineSeries {
|
|
101
|
+
const root = parseXml(serXml);
|
|
102
|
+
const node = findFirstDescendant(root, "ser");
|
|
103
|
+
if (!node) {
|
|
104
|
+
return emptySeries();
|
|
105
|
+
}
|
|
106
|
+
return parseLineSeriesNode(node);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Parse a pre-parsed `<c:ser>` element from a lineChart. Returns the common
|
|
111
|
+
* `Series` fields plus optional per-series smooth + marker overrides. Omits
|
|
112
|
+
* the optional keys entirely when absent so strict-type consumers can
|
|
113
|
+
* distinguish "not specified" from "explicitly false / none".
|
|
114
|
+
*/
|
|
115
|
+
export function parseLineSeriesNode(node: XmlElementNode): LineSeries {
|
|
116
|
+
const base = parseSeriesBase(node);
|
|
117
|
+
const smoothNode = findChildOptional(node, "smooth");
|
|
118
|
+
const smooth = readOnOff(smoothNode);
|
|
119
|
+
const marker = parseMarkerSpec(findChildOptional(node, "marker"));
|
|
120
|
+
|
|
121
|
+
const series: LineSeries = { ...base };
|
|
122
|
+
if (smooth !== undefined) series.smooth = smooth;
|
|
123
|
+
if (marker) series.marker = marker;
|
|
124
|
+
return series;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ---------------------------------------------------------------------------
|
|
128
|
+
// Marker
|
|
129
|
+
// ---------------------------------------------------------------------------
|
|
130
|
+
|
|
131
|
+
const MARKER_SYMBOLS: ReadonlySet<MarkerSpec["symbol"]> = new Set<
|
|
132
|
+
MarkerSpec["symbol"]
|
|
133
|
+
>([
|
|
134
|
+
"circle",
|
|
135
|
+
"square",
|
|
136
|
+
"diamond",
|
|
137
|
+
"triangle",
|
|
138
|
+
"x",
|
|
139
|
+
"star",
|
|
140
|
+
"dot",
|
|
141
|
+
"dash",
|
|
142
|
+
"plus",
|
|
143
|
+
"picture",
|
|
144
|
+
"none",
|
|
145
|
+
"auto",
|
|
146
|
+
]);
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Parse a `<c:marker>` element. Returns undefined when the node is absent.
|
|
150
|
+
* Returns a `MarkerSpec` with `symbol: "auto"` when the element is present
|
|
151
|
+
* but declares no symbol — OOXML's default for a marker with no subclass.
|
|
152
|
+
* Unknown symbol values also collapse to `"auto"` so renderers never see
|
|
153
|
+
* an unrecognised enum.
|
|
154
|
+
*/
|
|
155
|
+
export function parseMarkerSpec(
|
|
156
|
+
markerNode: XmlElementNode | undefined,
|
|
157
|
+
): MarkerSpec | undefined {
|
|
158
|
+
if (!markerNode) return undefined;
|
|
159
|
+
const symbolRaw = findChildOptional(markerNode, "symbol")?.attributes["val"];
|
|
160
|
+
const symbol: MarkerSpec["symbol"] =
|
|
161
|
+
symbolRaw && isMarkerSymbol(symbolRaw) ? symbolRaw : "auto";
|
|
162
|
+
const size = readIntVal(findChildOptional(markerNode, "size"));
|
|
163
|
+
const spPr = parseShapeProperties(findChildOptional(markerNode, "spPr"));
|
|
164
|
+
|
|
165
|
+
const marker: MarkerSpec = { symbol };
|
|
166
|
+
if (size !== undefined) marker.size = size;
|
|
167
|
+
if (spPr) marker.spPr = spPr;
|
|
168
|
+
return marker;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function isMarkerSymbol(value: string): value is MarkerSpec["symbol"] {
|
|
172
|
+
return MARKER_SYMBOLS.has(value as MarkerSpec["symbol"]);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// ---------------------------------------------------------------------------
|
|
176
|
+
// Pie series
|
|
177
|
+
// ---------------------------------------------------------------------------
|
|
178
|
+
|
|
179
|
+
/** Parse a `<c:ser>` XML fragment from a pieChart/doughnutChart into `PieSeries`. */
|
|
180
|
+
export function parsePieSeries(serXml: string): PieSeries {
|
|
181
|
+
const root = parseXml(serXml);
|
|
182
|
+
const node = findFirstDescendant(root, "ser");
|
|
183
|
+
if (!node) {
|
|
184
|
+
return { idx: 0, order: 0, values: [] };
|
|
185
|
+
}
|
|
186
|
+
return parsePieSeriesNode(node);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Parse a pre-parsed `<c:ser>` element from a pieChart/doughnutChart.
|
|
191
|
+
*
|
|
192
|
+
* PieSeries extends SeriesBase (idx/order/name/spPr) with:
|
|
193
|
+
* - values: the numeric cache from c:val/c:numRef/c:numCache (sparse, nulls preserved).
|
|
194
|
+
* - explosion: series-level default offset percent from c:explosion val.
|
|
195
|
+
* - dataPoints: optional per-slice overrides (c:dPt[idx]/{spPr,explosion,bubble3D}).
|
|
196
|
+
*
|
|
197
|
+
* Categories for pie live on `PieChartModel.categoryLabels`, not on the
|
|
198
|
+
* series — the group parser reads them from the first series' c:cat.
|
|
199
|
+
*/
|
|
200
|
+
export function parsePieSeriesNode(node: XmlElementNode): PieSeries {
|
|
201
|
+
const idx = readIntVal(findChildOptional(node, "idx")) ?? 0;
|
|
202
|
+
const order = readIntVal(findChildOptional(node, "order")) ?? 0;
|
|
203
|
+
const name = readSeriesName(findChildOptional(node, "tx"));
|
|
204
|
+
const values = extractNumCache(findChildOptional(node, "val"));
|
|
205
|
+
const spPr = parseShapeProperties(findChildOptional(node, "spPr"));
|
|
206
|
+
const explosion = readIntVal(findChildOptional(node, "explosion"));
|
|
207
|
+
const dataPoints = parseDataPointOverrides(node);
|
|
208
|
+
|
|
209
|
+
const series: PieSeries = { idx, order, values };
|
|
210
|
+
if (name !== undefined) series.name = name;
|
|
211
|
+
if (spPr) series.spPr = spPr;
|
|
212
|
+
if (explosion !== undefined) series.explosion = explosion;
|
|
213
|
+
if (dataPoints.length > 0) series.dataPoints = dataPoints;
|
|
214
|
+
return series;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// ---------------------------------------------------------------------------
|
|
218
|
+
// Data-point overrides (c:dPt)
|
|
219
|
+
// ---------------------------------------------------------------------------
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Walk `<c:dPt>` children of a `<c:ser>` element and return typed overrides.
|
|
223
|
+
*
|
|
224
|
+
* Each override must have a `<c:idx val="N">` child; malformed entries (no
|
|
225
|
+
* idx) are skipped silently — the well-formed entries still apply. Optional
|
|
226
|
+
* children captured: spPr (fill/stroke override), explosion (pie slice
|
|
227
|
+
* offset percent), bubble3D (tolerated on non-bubble charts), marker (line
|
|
228
|
+
* variants), invertIfNegative (bar variants).
|
|
229
|
+
*/
|
|
230
|
+
// ---------------------------------------------------------------------------
|
|
231
|
+
// Scatter series
|
|
232
|
+
// ---------------------------------------------------------------------------
|
|
233
|
+
|
|
234
|
+
/** Parse a `<c:ser>` XML fragment from a scatterChart into `ScatterSeries`. */
|
|
235
|
+
export function parseScatterSeries(serXml: string): ScatterSeries {
|
|
236
|
+
const root = parseXml(serXml);
|
|
237
|
+
const node = findFirstDescendant(root, "ser");
|
|
238
|
+
if (!node) {
|
|
239
|
+
return { idx: 0, order: 0, xValues: [], yValues: [] };
|
|
240
|
+
}
|
|
241
|
+
return parseScatterSeriesNode(node);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Parse a pre-parsed `<c:ser>` element from a scatterChart.
|
|
246
|
+
*
|
|
247
|
+
* ScatterSeries extends SeriesBase (idx/order/name/spPr) with:
|
|
248
|
+
* - xValues, yValues: parallel numeric caches from c:xVal and c:yVal.
|
|
249
|
+
* - smooth: per-series override (c:smooth).
|
|
250
|
+
* - marker: optional MarkerSpec (c:marker).
|
|
251
|
+
*
|
|
252
|
+
* Categories don't apply to scatter — both axes are numeric.
|
|
253
|
+
*/
|
|
254
|
+
export function parseScatterSeriesNode(node: XmlElementNode): ScatterSeries {
|
|
255
|
+
const idx = readIntVal(findChildOptional(node, "idx")) ?? 0;
|
|
256
|
+
const order = readIntVal(findChildOptional(node, "order")) ?? 0;
|
|
257
|
+
const name = readSeriesName(findChildOptional(node, "tx"));
|
|
258
|
+
const spPr = parseShapeProperties(findChildOptional(node, "spPr"));
|
|
259
|
+
const xValues = extractNumCache(findChildOptional(node, "xVal"));
|
|
260
|
+
const yValues = extractNumCache(findChildOptional(node, "yVal"));
|
|
261
|
+
const smooth = readOnOff(findChildOptional(node, "smooth"));
|
|
262
|
+
const marker = parseMarkerSpec(findChildOptional(node, "marker"));
|
|
263
|
+
|
|
264
|
+
const series: ScatterSeries = { idx, order, xValues, yValues };
|
|
265
|
+
if (name !== undefined) series.name = name;
|
|
266
|
+
if (spPr) series.spPr = spPr;
|
|
267
|
+
if (smooth !== undefined) series.smooth = smooth;
|
|
268
|
+
if (marker) series.marker = marker;
|
|
269
|
+
return series;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// ---------------------------------------------------------------------------
|
|
273
|
+
// Bubble series
|
|
274
|
+
// ---------------------------------------------------------------------------
|
|
275
|
+
|
|
276
|
+
/** Parse a `<c:ser>` XML fragment from a bubbleChart into `BubbleSeries`. */
|
|
277
|
+
export function parseBubbleSeries(serXml: string): BubbleSeries {
|
|
278
|
+
const root = parseXml(serXml);
|
|
279
|
+
const node = findFirstDescendant(root, "ser");
|
|
280
|
+
if (!node) {
|
|
281
|
+
return { idx: 0, order: 0, xValues: [], yValues: [], sizes: [] };
|
|
282
|
+
}
|
|
283
|
+
return parseBubbleSeriesNode(node);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Parse a pre-parsed `<c:ser>` element from a bubbleChart.
|
|
288
|
+
*
|
|
289
|
+
* BubbleSeries extends SeriesBase with x/y caches plus a third
|
|
290
|
+
* `sizes` channel from c:bubbleSize. Each triple (x, y, size) is one
|
|
291
|
+
* bubble; all three arrays are parallel-indexed, nulls preserved.
|
|
292
|
+
*/
|
|
293
|
+
export function parseBubbleSeriesNode(node: XmlElementNode): BubbleSeries {
|
|
294
|
+
const idx = readIntVal(findChildOptional(node, "idx")) ?? 0;
|
|
295
|
+
const order = readIntVal(findChildOptional(node, "order")) ?? 0;
|
|
296
|
+
const name = readSeriesName(findChildOptional(node, "tx"));
|
|
297
|
+
const spPr = parseShapeProperties(findChildOptional(node, "spPr"));
|
|
298
|
+
const xValues = extractNumCache(findChildOptional(node, "xVal"));
|
|
299
|
+
const yValues = extractNumCache(findChildOptional(node, "yVal"));
|
|
300
|
+
const sizes = extractNumCache(findChildOptional(node, "bubbleSize"));
|
|
301
|
+
|
|
302
|
+
const series: BubbleSeries = { idx, order, xValues, yValues, sizes };
|
|
303
|
+
if (name !== undefined) series.name = name;
|
|
304
|
+
if (spPr) series.spPr = spPr;
|
|
305
|
+
return series;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
export function parseDataPointOverrides(
|
|
309
|
+
serNode: XmlElementNode,
|
|
310
|
+
): DataPointOverride[] {
|
|
311
|
+
const overrides: DataPointOverride[] = [];
|
|
312
|
+
for (const child of serNode.children) {
|
|
313
|
+
if (child.type !== "element" || localName(child.name) !== "dPt") continue;
|
|
314
|
+
const idxNode = findChildOptional(child, "idx");
|
|
315
|
+
const idx = readIntVal(idxNode);
|
|
316
|
+
if (idx === undefined) continue;
|
|
317
|
+
|
|
318
|
+
const override: DataPointOverride = { idx };
|
|
319
|
+
const spPr = parseShapeProperties(findChildOptional(child, "spPr"));
|
|
320
|
+
if (spPr) override.spPr = spPr;
|
|
321
|
+
const marker = parseMarkerSpec(findChildOptional(child, "marker"));
|
|
322
|
+
if (marker) override.marker = marker;
|
|
323
|
+
const explosion = readIntVal(findChildOptional(child, "explosion"));
|
|
324
|
+
if (explosion !== undefined) override.explosion = explosion;
|
|
325
|
+
const invert = readOnOff(findChildOptional(child, "invertIfNegative"));
|
|
326
|
+
if (invert !== undefined) override.invertIfNegative = invert;
|
|
327
|
+
const bubble3D = readOnOff(findChildOptional(child, "bubble3D"));
|
|
328
|
+
if (bubble3D !== undefined) override.bubble3D = bubble3D;
|
|
329
|
+
|
|
330
|
+
overrides.push(override);
|
|
331
|
+
}
|
|
332
|
+
return overrides;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function readSeriesName(txNode: XmlElementNode | undefined): string | undefined {
|
|
336
|
+
if (!txNode) return undefined;
|
|
337
|
+
|
|
338
|
+
// Case 1: <c:tx><c:strRef><c:strCache>…<c:v>Name</c:v>…
|
|
339
|
+
const strRef = findChildOptional(txNode, "strRef");
|
|
340
|
+
if (strRef) {
|
|
341
|
+
const cache = findFirstDescendant(strRef, "strCache");
|
|
342
|
+
if (cache) {
|
|
343
|
+
for (const child of cache.children) {
|
|
344
|
+
if (child.type !== "element" || localName(child.name) !== "pt") continue;
|
|
345
|
+
const v = findChildOptional(child, "v");
|
|
346
|
+
if (v) return textContent(v);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
// strCache missing — fall back to any c:v descendant under the strRef.
|
|
350
|
+
const directV = findFirstDescendant(strRef, "v");
|
|
351
|
+
if (directV) return textContent(directV);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Case 2: <c:tx><c:v>Name</c:v> (inline literal).
|
|
355
|
+
const direct = findChildOptional(txNode, "v");
|
|
356
|
+
if (direct) return textContent(direct);
|
|
357
|
+
|
|
358
|
+
// Case 3: <c:tx><c:rich><a:p><a:r><a:t>Name</a:t>… — plain-text collapse.
|
|
359
|
+
const rich = findChildOptional(txNode, "rich");
|
|
360
|
+
if (rich) {
|
|
361
|
+
const firstT = findFirstDescendant(rich, "t");
|
|
362
|
+
if (firstT) return textContent(firstT);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
return undefined;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// ---------------------------------------------------------------------------
|
|
369
|
+
// Cache extraction
|
|
370
|
+
// ---------------------------------------------------------------------------
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Extract a sparse numeric cache. Returns an array of `ptCount` length
|
|
374
|
+
* filled with `null`, with each `<c:pt idx="N"><c:v>value</c:v></c:pt>`
|
|
375
|
+
* populating index N. Values that fail to parse as finite numbers become
|
|
376
|
+
* `null` as well.
|
|
377
|
+
*
|
|
378
|
+
* The `expected` parameter lets callers bump the array length when
|
|
379
|
+
* `c:ptCount` is absent (e.g. when the cache is a companion to another
|
|
380
|
+
* series whose length is known).
|
|
381
|
+
*/
|
|
382
|
+
export function extractNumCache(
|
|
383
|
+
container: XmlElementNode | undefined,
|
|
384
|
+
expected?: number,
|
|
385
|
+
): Array<number | null> {
|
|
386
|
+
if (!container) return [];
|
|
387
|
+
const cache =
|
|
388
|
+
findFirstDescendant(container, "numCache") ??
|
|
389
|
+
findFirstDescendant(container, "strCache");
|
|
390
|
+
if (!cache) return [];
|
|
391
|
+
const ptCount = readIntVal(findChildOptional(cache, "ptCount")) ?? 0;
|
|
392
|
+
const length = Math.max(ptCount, expected ?? 0);
|
|
393
|
+
const out: Array<number | null> = new Array(length).fill(null);
|
|
394
|
+
for (const child of cache.children) {
|
|
395
|
+
if (child.type !== "element" || localName(child.name) !== "pt") continue;
|
|
396
|
+
const idxRaw = child.attributes["idx"];
|
|
397
|
+
const idx = idxRaw ? Number.parseInt(idxRaw, 10) : Number.NaN;
|
|
398
|
+
if (!Number.isFinite(idx) || idx < 0 || idx >= out.length) continue;
|
|
399
|
+
const vNode = findChildOptional(child, "v");
|
|
400
|
+
if (!vNode) continue;
|
|
401
|
+
const n = Number.parseFloat(textContent(vNode));
|
|
402
|
+
if (Number.isFinite(n)) out[idx] = n;
|
|
403
|
+
}
|
|
404
|
+
return out;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Extract a sparse string cache. Mirrors `extractNumCache` but fills
|
|
409
|
+
* missing indices with empty strings. Falls back to `numCache` if the
|
|
410
|
+
* container only has numeric data (so a numRef-backed category axis still
|
|
411
|
+
* yields stringified labels).
|
|
412
|
+
*/
|
|
413
|
+
export function extractStrCache(
|
|
414
|
+
container: XmlElementNode | undefined,
|
|
415
|
+
): string[] {
|
|
416
|
+
if (!container) return [];
|
|
417
|
+
const cache =
|
|
418
|
+
findFirstDescendant(container, "strCache") ??
|
|
419
|
+
findFirstDescendant(container, "numCache");
|
|
420
|
+
if (!cache) return [];
|
|
421
|
+
const ptCount = readIntVal(findChildOptional(cache, "ptCount")) ?? 0;
|
|
422
|
+
const out: string[] = new Array(ptCount).fill("");
|
|
423
|
+
for (const child of cache.children) {
|
|
424
|
+
if (child.type !== "element" || localName(child.name) !== "pt") continue;
|
|
425
|
+
const idxRaw = child.attributes["idx"];
|
|
426
|
+
const idx = idxRaw ? Number.parseInt(idxRaw, 10) : Number.NaN;
|
|
427
|
+
if (!Number.isFinite(idx) || idx < 0 || idx >= out.length) continue;
|
|
428
|
+
const vNode = findChildOptional(child, "v");
|
|
429
|
+
if (!vNode) continue;
|
|
430
|
+
out[idx] = textContent(vNode);
|
|
431
|
+
}
|
|
432
|
+
return out;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// ---------------------------------------------------------------------------
|
|
436
|
+
// Shape properties (spPr) — fills and strokes
|
|
437
|
+
// ---------------------------------------------------------------------------
|
|
438
|
+
|
|
439
|
+
export function parseShapeProperties(
|
|
440
|
+
spPrNode: XmlElementNode | undefined,
|
|
441
|
+
): ShapeProperties | undefined {
|
|
442
|
+
if (!spPrNode) return undefined;
|
|
443
|
+
const fill = parseFill(spPrNode);
|
|
444
|
+
const stroke = parseStroke(findChildOptional(spPrNode, "ln"));
|
|
445
|
+
if (!fill && !stroke) return undefined;
|
|
446
|
+
const result: ShapeProperties = {};
|
|
447
|
+
if (fill) result.fill = fill;
|
|
448
|
+
if (stroke) result.stroke = stroke;
|
|
449
|
+
return result;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function parseFill(spPrNode: XmlElementNode): FillSpec | undefined {
|
|
453
|
+
// Explicit fill-none sentinel.
|
|
454
|
+
if (findChildOptional(spPrNode, "noFill")) {
|
|
455
|
+
return { kind: "none" };
|
|
456
|
+
}
|
|
457
|
+
const solid = findChildOptional(spPrNode, "solidFill");
|
|
458
|
+
if (solid) {
|
|
459
|
+
const color = parseColorRef(solid);
|
|
460
|
+
if (color) return { kind: "solid", color };
|
|
461
|
+
}
|
|
462
|
+
// Gradient and pattern fills are not resolved in Stage 1 — they stay
|
|
463
|
+
// as `{ kind: "none" }` placeholders so the downstream renderer at least
|
|
464
|
+
// produces a consistent, non-crashing surface. Stage 2 replaces this.
|
|
465
|
+
if (findChildOptional(spPrNode, "gradFill")) {
|
|
466
|
+
return { kind: "none" };
|
|
467
|
+
}
|
|
468
|
+
if (findChildOptional(spPrNode, "pattFill")) {
|
|
469
|
+
return { kind: "none" };
|
|
470
|
+
}
|
|
471
|
+
return undefined;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function parseStroke(lnNode: XmlElementNode | undefined): StrokeSpec | undefined {
|
|
475
|
+
if (!lnNode) return undefined;
|
|
476
|
+
const stroke: StrokeSpec = {};
|
|
477
|
+
const widthAttr = lnNode.attributes["w"];
|
|
478
|
+
if (widthAttr) {
|
|
479
|
+
const w = Number.parseInt(widthAttr, 10);
|
|
480
|
+
if (Number.isFinite(w)) stroke.widthEmu = w;
|
|
481
|
+
}
|
|
482
|
+
if (findChildOptional(lnNode, "noFill")) {
|
|
483
|
+
stroke.noFill = true;
|
|
484
|
+
}
|
|
485
|
+
const solid = findChildOptional(lnNode, "solidFill");
|
|
486
|
+
if (solid) {
|
|
487
|
+
const color = parseColorRef(solid);
|
|
488
|
+
if (color) stroke.color = color;
|
|
489
|
+
}
|
|
490
|
+
const prstDash = findChildOptional(lnNode, "prstDash");
|
|
491
|
+
if (prstDash) {
|
|
492
|
+
const dashVal = prstDash.attributes["val"];
|
|
493
|
+
switch (dashVal) {
|
|
494
|
+
case "solid":
|
|
495
|
+
case "dash":
|
|
496
|
+
case "dashDot":
|
|
497
|
+
case "lgDash":
|
|
498
|
+
case "lgDashDot":
|
|
499
|
+
case "sysDash":
|
|
500
|
+
case "sysDashDot":
|
|
501
|
+
stroke.dash = dashVal;
|
|
502
|
+
break;
|
|
503
|
+
default:
|
|
504
|
+
break;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
if (
|
|
508
|
+
stroke.widthEmu === undefined &&
|
|
509
|
+
stroke.noFill === undefined &&
|
|
510
|
+
stroke.color === undefined &&
|
|
511
|
+
stroke.dash === undefined
|
|
512
|
+
) {
|
|
513
|
+
return undefined;
|
|
514
|
+
}
|
|
515
|
+
return stroke;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* Parse a color reference child out of a fill wrapper (solidFill, stroke
|
|
520
|
+
* solidFill, etc.). Supports srgbClr and schemeClr with the full OOXML
|
|
521
|
+
* modifier set (lumMod, lumOff, shade, tint, satMod, hueMod, alpha).
|
|
522
|
+
*/
|
|
523
|
+
function parseColorRef(wrapper: XmlElementNode): ColorRef | undefined {
|
|
524
|
+
for (const child of wrapper.children) {
|
|
525
|
+
if (child.type !== "element") continue;
|
|
526
|
+
const local = localName(child.name);
|
|
527
|
+
if (local === "srgbClr") {
|
|
528
|
+
const val = child.attributes["val"];
|
|
529
|
+
if (!val) continue;
|
|
530
|
+
return { kind: "srgb", value: `#${val.toUpperCase()}` };
|
|
531
|
+
}
|
|
532
|
+
if (local === "schemeClr") {
|
|
533
|
+
const val = child.attributes["val"];
|
|
534
|
+
if (!val) continue;
|
|
535
|
+
const mods = parseColorMods(child);
|
|
536
|
+
const ref: ColorRef = { kind: "scheme", value: val };
|
|
537
|
+
if (mods.length > 0) ref.mods = mods;
|
|
538
|
+
return ref;
|
|
539
|
+
}
|
|
540
|
+
if (local === "sysClr") {
|
|
541
|
+
// Take the resolved lastClr value; treat as sRGB to stay in-band.
|
|
542
|
+
const lastClr = child.attributes["lastClr"];
|
|
543
|
+
if (lastClr) return { kind: "srgb", value: `#${lastClr.toUpperCase()}` };
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
return undefined;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
function parseColorMods(colorNode: XmlElementNode): ColorMod[] {
|
|
550
|
+
const mods: ColorMod[] = [];
|
|
551
|
+
for (const child of colorNode.children) {
|
|
552
|
+
if (child.type !== "element") continue;
|
|
553
|
+
const local = localName(child.name);
|
|
554
|
+
if (
|
|
555
|
+
local !== "lumMod" &&
|
|
556
|
+
local !== "lumOff" &&
|
|
557
|
+
local !== "shade" &&
|
|
558
|
+
local !== "tint" &&
|
|
559
|
+
local !== "satMod" &&
|
|
560
|
+
local !== "hueMod" &&
|
|
561
|
+
local !== "alpha"
|
|
562
|
+
) {
|
|
563
|
+
continue;
|
|
564
|
+
}
|
|
565
|
+
const value = readFloatVal(child);
|
|
566
|
+
if (value === undefined) continue;
|
|
567
|
+
mods.push({ kind: local, value });
|
|
568
|
+
}
|
|
569
|
+
return mods;
|
|
570
|
+
}
|