@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.
Files changed (80) hide show
  1. package/README.md +16 -11
  2. package/package.json +30 -41
  3. package/src/api/public-types.ts +199 -13
  4. package/src/compare/diff-engine.ts +4 -0
  5. package/src/core/commands/add-scope.ts +257 -0
  6. package/src/core/commands/formatting-commands.ts +2 -0
  7. package/src/core/commands/index.ts +9 -1
  8. package/src/core/commands/text-commands.ts +3 -1
  9. package/src/core/schema/text-schema.ts +95 -1
  10. package/src/core/selection/anchor-conversion.ts +112 -0
  11. package/src/core/selection/review-anchors.ts +108 -3
  12. package/src/core/state/text-transaction.ts +103 -7
  13. package/src/internal/harness-debug-ports.ts +168 -0
  14. package/src/io/chart-preview-resolver.ts +59 -1
  15. package/src/io/docx-session.ts +226 -38
  16. package/src/io/export/serialize-main-document.ts +46 -0
  17. package/src/io/export/serialize-paragraph-formatting.ts +8 -0
  18. package/src/io/export/serialize-run-formatting.ts +10 -1
  19. package/src/io/export/serialize-settings.ts +421 -0
  20. package/src/io/export/serialize-styles.ts +10 -0
  21. package/src/io/normalize/normalize-text.ts +1 -0
  22. package/src/io/ooxml/chart/chart-style-table.ts +543 -0
  23. package/src/io/ooxml/chart/color-palette.ts +101 -0
  24. package/src/io/ooxml/chart/compose-series-color.ts +147 -0
  25. package/src/io/ooxml/chart/parse-axis.ts +277 -0
  26. package/src/io/ooxml/chart/parse-chart-space.ts +885 -0
  27. package/src/io/ooxml/chart/parse-series.ts +635 -0
  28. package/src/io/ooxml/chart/resolve-color.ts +261 -0
  29. package/src/io/ooxml/chart/types.ts +439 -0
  30. package/src/io/ooxml/parse-block-structure.ts +99 -0
  31. package/src/io/ooxml/parse-complex-content.ts +90 -2
  32. package/src/io/ooxml/parse-main-document.ts +156 -1
  33. package/src/io/ooxml/parse-paragraph-formatting.ts +46 -0
  34. package/src/io/ooxml/parse-run-formatting.ts +49 -0
  35. package/src/io/ooxml/parse-scope-markers.ts +184 -0
  36. package/src/io/ooxml/parse-settings-blueprint.ts +349 -0
  37. package/src/io/ooxml/parse-settings.ts +97 -1
  38. package/src/io/ooxml/parse-styles.ts +65 -0
  39. package/src/io/ooxml/parse-theme.ts +2 -127
  40. package/src/io/ooxml/property-grab-bag.ts +211 -0
  41. package/src/io/ooxml/xml-attr-helpers.ts +59 -1
  42. package/src/io/ooxml/xml-parser.ts +142 -0
  43. package/src/model/canonical-document.ts +160 -0
  44. package/src/model/scope-markers.ts +144 -0
  45. package/src/runtime/collab/base-doc-fingerprint.ts +99 -0
  46. package/src/runtime/collab/checkpoint-election.ts +75 -0
  47. package/src/runtime/collab/checkpoint-scheduler.ts +204 -0
  48. package/src/runtime/collab/checkpoint-store.ts +115 -0
  49. package/src/runtime/collab/event-types.ts +27 -0
  50. package/src/runtime/collab/index.ts +29 -0
  51. package/src/runtime/collab/remote-cursor-awareness.ts +167 -0
  52. package/src/runtime/collab/runtime-collab-sync.ts +330 -0
  53. package/src/runtime/collab/workflow-shared.ts +247 -0
  54. package/src/runtime/document-locations.ts +1 -9
  55. package/src/runtime/document-outline.ts +1 -9
  56. package/src/runtime/document-runtime.ts +288 -65
  57. package/src/runtime/editor-surface/capabilities.ts +63 -50
  58. package/src/runtime/hyperlink-color-resolver.ts +119 -0
  59. package/src/runtime/layout/layout-engine-version.ts +8 -1
  60. package/src/runtime/prerender/cache-envelope.ts +19 -7
  61. package/src/runtime/prerender/cache-key.ts +25 -14
  62. package/src/runtime/prerender/canonical-document-hash.ts +63 -0
  63. package/src/runtime/prerender/customxml-cache.ts +211 -0
  64. package/src/runtime/prerender/customxml-probe.ts +78 -0
  65. package/src/runtime/prerender/prerender-document.ts +74 -7
  66. package/src/runtime/scope-resolver.ts +148 -0
  67. package/src/runtime/scope-tag-registry.ts +10 -0
  68. package/src/runtime/surface-projection.ts +102 -37
  69. package/src/runtime/theme-color-resolver.ts +188 -0
  70. package/src/runtime/workflow-markup.ts +7 -18
  71. package/src/ui/WordReviewEditor.tsx +48 -2
  72. package/src/ui/editor-runtime-boundary.ts +42 -1
  73. package/src/ui/headless/selection-helpers.ts +10 -23
  74. package/src/ui/runtime-shortcut-dispatch.ts +12 -7
  75. package/src/ui/unsupported-previews-policy.ts +23 -0
  76. package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +10 -0
  77. package/src/ui-tailwind/editor-surface/perf-probe.ts +1 -0
  78. package/src/ui-tailwind/page-stack/tw-page-stack-chrome-layer.tsx +47 -0
  79. package/src/ui-tailwind/page-stack/use-visible-block-range.ts +88 -0
  80. package/src/ui-tailwind/tw-review-workspace.tsx +16 -1
@@ -0,0 +1,147 @@
1
+ /**
2
+ * Compose the final sRGB color for a series in a parsed `ChartModel`.
3
+ *
4
+ * This is the Stage-2 call site that stitches together the three pieces
5
+ * the Stage-2 slices shipped:
6
+ *
7
+ * explicit `c:ser/spPr/fill.color` override (`parse-series.ts`)
8
+ * ↓ (fallback when no override)
9
+ * `paletteColorRef(style.seriesColorMode, seriesIdx)` (`color-palette.ts`)
10
+ * ↓
11
+ * `resolveColor(ref, theme)` (`resolve-color.ts`)
12
+ * ↓
13
+ * `#RRGGBB`
14
+ *
15
+ * Stage 3 + 4 renderers consume `composeSeriesColor` rather than reaching
16
+ * into the three helpers individually. Keeping the composition in one
17
+ * place guarantees the cascade order stays correct and gives us a stable
18
+ * unit-test surface.
19
+ *
20
+ * `deriveResolvedColors(model, theme)` pre-computes the full series list
21
+ * so renderers can render without touching the chart-style table in hot
22
+ * paths.
23
+ */
24
+
25
+ import { getChartStyle, resolveChartStyleId } from "./chart-style-table.ts";
26
+ import { paletteColorRef } from "./color-palette.ts";
27
+ import { resolveColor } from "./resolve-color.ts";
28
+ import type { ChartModel, ColorRef } from "./types.ts";
29
+ import type { ResolvedTheme } from "../../../model/canonical-document.ts";
30
+
31
+ /**
32
+ * Compose a single series's final sRGB color.
33
+ *
34
+ * - `model` may be any ChartModel variant. For the `unsupported` /
35
+ * `combo` variants the seriesIdx refers to the top-level series list
36
+ * (bar/line/area/pie/scatter/bubble) flattened in declaration order.
37
+ * Combo callers should pass the sub-group's model directly for the
38
+ * cleanest result.
39
+ * - `seriesIdx` is the index into the model's `series` array (or
40
+ * `categoryLabels` for pie when `varyColors=true`).
41
+ * - `theme` is the resolved workbook theme; pass `{ colors: {} }` for
42
+ * tests that want fallback-only behavior.
43
+ *
44
+ * Returns an always-valid `#RRGGBB` string. Malformed inputs fall through
45
+ * to the resolver's fallback color (#808080).
46
+ */
47
+ export function composeSeriesColor(
48
+ model: ChartModel,
49
+ theme: ResolvedTheme,
50
+ seriesIdx: number,
51
+ ): string {
52
+ // 1. Explicit per-series override takes priority.
53
+ const override = readSeriesOverrideColor(model, seriesIdx);
54
+ if (override) return resolveColor(override, theme);
55
+
56
+ // 2. Fall through to the chart-style's palette.
57
+ const style = getChartStyle(resolveChartStyleId(model.styleId));
58
+ const ref = paletteColorRef(style.seriesColorMode, seriesIdx);
59
+ return resolveColor(ref, theme);
60
+ }
61
+
62
+ /**
63
+ * Pre-compute the resolved sRGB color for every series in `model`.
64
+ * Returns an empty array for `unsupported` charts. For pie/doughnut the
65
+ * array length matches the number of slices (categoryLabels), not the
66
+ * number of series (pie has exactly one series).
67
+ */
68
+ export function deriveResolvedColors(
69
+ model: ChartModel,
70
+ theme: ResolvedTheme,
71
+ ): string[] {
72
+ const out: string[] = [];
73
+ const n = countPaletteSlots(model);
74
+ for (let i = 0; i < n; i++) {
75
+ out.push(composeSeriesColor(model, theme, i));
76
+ }
77
+ return out;
78
+ }
79
+
80
+ // ---------------------------------------------------------------------------
81
+ // Internals
82
+ // ---------------------------------------------------------------------------
83
+
84
+ /**
85
+ * How many distinct colors the renderer needs. For pie charts with
86
+ * varyColors (the default), we need one color per slice; for all other
87
+ * families, one per series.
88
+ */
89
+ function countPaletteSlots(model: ChartModel): number {
90
+ switch (model.kind) {
91
+ case "pie":
92
+ return model.varyColors && model.series.length > 0
93
+ ? model.series[0]!.values.length
94
+ : model.series.length;
95
+ case "bar":
96
+ case "line":
97
+ case "area":
98
+ case "scatter":
99
+ case "bubble":
100
+ return model.series.length;
101
+ case "combo": {
102
+ let total = 0;
103
+ for (const group of model.groups) total += group.series.length;
104
+ return total;
105
+ }
106
+ case "unsupported":
107
+ return 0;
108
+ }
109
+ }
110
+
111
+ /**
112
+ * Read an explicit per-series fill color override. Pie-only: also checks
113
+ * per-slice dataPoint overrides when `seriesIdx` is a slice index.
114
+ */
115
+ function readSeriesOverrideColor(
116
+ model: ChartModel,
117
+ seriesIdx: number,
118
+ ): ColorRef | undefined {
119
+ if (model.kind === "unsupported" || model.kind === "combo") return undefined;
120
+
121
+ // Pie: per-slice dPt.spPr.fill takes priority.
122
+ if (model.kind === "pie") {
123
+ const pieSeries = model.series[0];
124
+ if (!pieSeries) return undefined;
125
+ const dp = pieSeries.dataPoints?.find((d) => d.idx === seriesIdx);
126
+ const dpFill = dp?.spPr?.fill;
127
+ if (dpFill && dpFill.kind === "solid") return dpFill.color;
128
+ // Pie with varyColors=false falls through to the series spPr color.
129
+ if (!model.varyColors) {
130
+ const seriesFill = pieSeries.spPr?.fill;
131
+ if (seriesFill && seriesFill.kind === "solid") return seriesFill.color;
132
+ }
133
+ return undefined;
134
+ }
135
+
136
+ // Bar / line / area / scatter / bubble: read series[idx].spPr.fill.
137
+ const series = model.series[seriesIdx];
138
+ if (!series) return undefined;
139
+ const fill = series.spPr?.fill;
140
+ if (fill && fill.kind === "solid") return fill.color;
141
+ // Gradient and pattern: pick the first stop as a solid approximation
142
+ // until Stage 4 renderers handle them natively.
143
+ if (fill && fill.kind === "gradient" && fill.stops.length > 0) {
144
+ return fill.stops[0]!.color;
145
+ }
146
+ return undefined;
147
+ }
@@ -0,0 +1,277 @@
1
+ /**
2
+ * Parse chart axis elements.
3
+ *
4
+ * OOXML defines four axis element names:
5
+ * c:catAx (category) c:valAx (value) c:dateAx (date) c:serAx (series)
6
+ *
7
+ * They share a common set of child elements (c:axId, c:delete, c:axPos,
8
+ * c:title, c:majorGridlines, c:minorGridlines, c:numFmt, c:crossAx, etc.)
9
+ * plus axis-specific extensions. This parser produces a discriminated
10
+ * `Axis` union that records which root element was seen.
11
+ *
12
+ * Note: c:delete has inverted semantics — `val="1"` means *invisible* and
13
+ * a missing element means *visible*. We store the normal-sense boolean
14
+ * on `AxisBase.visible`.
15
+ */
16
+
17
+ import {
18
+ findChildOptional,
19
+ localName,
20
+ readFloatVal,
21
+ readIntVal,
22
+ readStringAttr,
23
+ textContent,
24
+ } from "../xml-attr-helpers.ts";
25
+ import type { XmlElementNode } from "../xml-element.ts";
26
+ import { parseXml } from "../xml-parser.ts";
27
+
28
+ import type {
29
+ Axis,
30
+ AxisBase,
31
+ CategoryAxis,
32
+ DateAxis,
33
+ SeriesAxis,
34
+ Title,
35
+ ValueAxis,
36
+ } from "./types.ts";
37
+
38
+ const AXIS_LOCAL_NAMES = new Set(["catAx", "valAx", "dateAx", "serAx"]);
39
+
40
+ /** Parse an axis element from raw XML. Throws if the root is not a known axis. */
41
+ export function parseAxis(xml: string): Axis {
42
+ const root = parseXml(xml);
43
+ let axisNode: XmlElementNode | undefined;
44
+ for (const child of root.children) {
45
+ if (child.type !== "element") continue;
46
+ if (AXIS_LOCAL_NAMES.has(localName(child.name))) {
47
+ axisNode = child;
48
+ break;
49
+ }
50
+ }
51
+ if (!axisNode) {
52
+ throw new Error(
53
+ `parseAxis: expected one of catAx/valAx/dateAx/serAx at root`,
54
+ );
55
+ }
56
+ return parseAxisNode(axisNode);
57
+ }
58
+
59
+ /** Dispatch to the axis-kind-specific parser based on the element name. */
60
+ export function parseAxisNode(node: XmlElementNode): Axis {
61
+ const base = parseAxisBase(node);
62
+ switch (localName(node.name)) {
63
+ case "catAx":
64
+ return parseCategoryAxisFields(node, base);
65
+ case "valAx":
66
+ return parseValueAxisFields(node, base);
67
+ case "dateAx":
68
+ return parseDateAxisFields(node, base);
69
+ case "serAx":
70
+ return { ...base, kind: "series" } satisfies SeriesAxis;
71
+ default:
72
+ throw new Error(`parseAxisNode: unsupported axis element ${node.name}`);
73
+ }
74
+ }
75
+
76
+ function parseAxisBase(node: XmlElementNode): AxisBase {
77
+ const axIdNode = findChildOptional(node, "axId");
78
+ const id = axIdNode ? (readStringAttr(axIdNode, "val") ?? "") : "";
79
+ const deleteNode = findChildOptional(node, "delete");
80
+ // c:delete has inverted semantics.
81
+ const deleteVal = deleteNode?.attributes["val"] ?? "0";
82
+ const visible = deleteVal !== "1" && deleteVal.toLowerCase() !== "true";
83
+
84
+ const positionRaw = findChildOptional(node, "axPos")?.attributes["val"] ?? "b";
85
+ const position: AxisBase["position"] =
86
+ positionRaw === "b" || positionRaw === "t" || positionRaw === "l" || positionRaw === "r"
87
+ ? positionRaw
88
+ : "b";
89
+
90
+ const crossAxisId = findChildOptional(node, "crossAx")?.attributes["val"];
91
+ const crossesAt = parseCrossesAt(findChildOptional(node, "crosses"));
92
+
93
+ const base: AxisBase = {
94
+ id,
95
+ position,
96
+ visible,
97
+ };
98
+ if (crossAxisId !== undefined) base.crossAxisId = crossAxisId;
99
+ if (crossesAt !== undefined) base.crossesAt = crossesAt;
100
+
101
+ const title = parseAxisTitle(findChildOptional(node, "title"));
102
+ if (title) base.title = title;
103
+
104
+ if (findChildOptional(node, "majorGridlines")) base.majorGridlines = true;
105
+ if (findChildOptional(node, "minorGridlines")) base.minorGridlines = true;
106
+
107
+ const majorUnit = readFloatVal(findChildOptional(node, "majorUnit"));
108
+ if (majorUnit !== undefined) base.majorUnit = majorUnit;
109
+ const minorUnit = readFloatVal(findChildOptional(node, "minorUnit"));
110
+ if (minorUnit !== undefined) base.minorUnit = minorUnit;
111
+
112
+ const numFmtNode = findChildOptional(node, "numFmt");
113
+ if (numFmtNode) {
114
+ const formatCode = numFmtNode.attributes["formatCode"];
115
+ if (formatCode !== undefined) base.numberFormat = formatCode;
116
+ }
117
+
118
+ return base;
119
+ }
120
+
121
+ function parseCrossesAt(
122
+ crossesNode: XmlElementNode | undefined,
123
+ ): number | "autoZero" | "max" | "min" | undefined {
124
+ if (!crossesNode) return undefined;
125
+ const val = crossesNode.attributes["val"];
126
+ if (val === undefined) return undefined;
127
+ if (val === "autoZero" || val === "max" || val === "min") return val;
128
+ const n = Number.parseFloat(val);
129
+ return Number.isFinite(n) ? n : undefined;
130
+ }
131
+
132
+ function parseAxisTitle(titleNode: XmlElementNode | undefined): Title | undefined {
133
+ if (!titleNode) return undefined;
134
+ let text: string | undefined;
135
+ const tx = findChildOptional(titleNode, "tx");
136
+ if (tx) {
137
+ const rich = findChildOptional(tx, "rich");
138
+ if (rich) {
139
+ const texts: string[] = [];
140
+ const collectT = (children: XmlElementNode["children"]): void => {
141
+ for (const c of children) {
142
+ if (c.type !== "element") continue;
143
+ if (localName(c.name) === "t") {
144
+ texts.push(textContent(c));
145
+ } else {
146
+ collectT(c.children);
147
+ }
148
+ }
149
+ };
150
+ collectT(rich.children);
151
+ text = texts.join("");
152
+ } else {
153
+ const v = findChildOptional(tx, "v");
154
+ if (v) text = textContent(v);
155
+ }
156
+ }
157
+ const overlay = findChildOptional(titleNode, "overlay")?.attributes["val"] === "1";
158
+ const title: Title = { overlay };
159
+ if (text) title.text = text;
160
+ return title;
161
+ }
162
+
163
+ // ---------------------------------------------------------------------------
164
+ // Category axis
165
+ // ---------------------------------------------------------------------------
166
+
167
+ function parseCategoryAxisFields(
168
+ node: XmlElementNode,
169
+ base: AxisBase,
170
+ ): CategoryAxis {
171
+ const auto = findChildOptional(node, "auto")?.attributes["val"] === "1";
172
+ const labelAlignRaw = findChildOptional(node, "lblAlgn")?.attributes["val"];
173
+ const labelAlign =
174
+ labelAlignRaw === "ctr" || labelAlignRaw === "l" || labelAlignRaw === "r"
175
+ ? labelAlignRaw
176
+ : undefined;
177
+ const labelOffset = readIntVal(findChildOptional(node, "lblOffset"));
178
+ const tickMarkRaw = findChildOptional(node, "majorTickMark")?.attributes["val"];
179
+ const tickMark =
180
+ tickMarkRaw === "none" ||
181
+ tickMarkRaw === "in" ||
182
+ tickMarkRaw === "out" ||
183
+ tickMarkRaw === "cross"
184
+ ? tickMarkRaw
185
+ : undefined;
186
+ const tickLabelSkip = readIntVal(findChildOptional(node, "tickLblSkip"));
187
+ const tickMarkSkip = readIntVal(findChildOptional(node, "tickMarkSkip"));
188
+
189
+ const axis: CategoryAxis = {
190
+ ...base,
191
+ kind: "category",
192
+ auto,
193
+ categoryLabels: [],
194
+ };
195
+ if (labelAlign) axis.labelAlign = labelAlign;
196
+ if (labelOffset !== undefined) axis.labelOffset = labelOffset;
197
+ if (tickMark) axis.tickMark = tickMark;
198
+ if (tickLabelSkip !== undefined) axis.tickLabelSkip = tickLabelSkip;
199
+ if (tickMarkSkip !== undefined) axis.tickMarkSkip = tickMarkSkip;
200
+ return axis;
201
+ }
202
+
203
+ // ---------------------------------------------------------------------------
204
+ // Value axis
205
+ // ---------------------------------------------------------------------------
206
+
207
+ function parseValueAxisFields(
208
+ node: XmlElementNode,
209
+ base: AxisBase,
210
+ ): ValueAxis {
211
+ const scaling = findChildOptional(node, "scaling");
212
+ const min = readFloatVal(scaling ? findChildOptional(scaling, "min") : undefined);
213
+ const max = readFloatVal(scaling ? findChildOptional(scaling, "max") : undefined);
214
+ const logBase = readFloatVal(
215
+ scaling ? findChildOptional(scaling, "logBase") : undefined,
216
+ );
217
+ const orientation = scaling
218
+ ? findChildOptional(scaling, "orientation")?.attributes["val"]
219
+ : undefined;
220
+ const reverse = orientation === "maxMin";
221
+
222
+ const crossBetweenRaw = findChildOptional(node, "crossBetween")?.attributes["val"];
223
+ const crossBetween =
224
+ crossBetweenRaw === "between" || crossBetweenRaw === "midCat"
225
+ ? crossBetweenRaw
226
+ : undefined;
227
+
228
+ const axis: ValueAxis = {
229
+ ...base,
230
+ kind: "value",
231
+ reverse,
232
+ };
233
+ if (min !== undefined) axis.min = min;
234
+ if (max !== undefined) axis.max = max;
235
+ if (logBase !== undefined) axis.logBase = logBase;
236
+ if (crossBetween) axis.crossBetween = crossBetween;
237
+ return axis;
238
+ }
239
+
240
+ // ---------------------------------------------------------------------------
241
+ // Date axis
242
+ // ---------------------------------------------------------------------------
243
+
244
+ function parseDateAxisFields(
245
+ node: XmlElementNode,
246
+ base: AxisBase,
247
+ ): DateAxis {
248
+ const scaling = findChildOptional(node, "scaling");
249
+ const min = readFloatVal(scaling ? findChildOptional(scaling, "min") : undefined);
250
+ const max = readFloatVal(scaling ? findChildOptional(scaling, "max") : undefined);
251
+ const baseTimeUnit = asTimeUnit(
252
+ findChildOptional(node, "baseTimeUnit")?.attributes["val"],
253
+ );
254
+ const majorTimeUnit = asTimeUnit(
255
+ findChildOptional(node, "majorTimeUnit")?.attributes["val"],
256
+ );
257
+ const minorTimeUnit = asTimeUnit(
258
+ findChildOptional(node, "minorTimeUnit")?.attributes["val"],
259
+ );
260
+ const axis: DateAxis = {
261
+ ...base,
262
+ kind: "date",
263
+ };
264
+ if (min !== undefined) axis.min = min;
265
+ if (max !== undefined) axis.max = max;
266
+ if (baseTimeUnit) axis.baseTimeUnit = baseTimeUnit;
267
+ if (majorTimeUnit) axis.majorTimeUnit = majorTimeUnit;
268
+ if (minorTimeUnit) axis.minorTimeUnit = minorTimeUnit;
269
+ return axis;
270
+ }
271
+
272
+ function asTimeUnit(
273
+ raw: string | undefined,
274
+ ): "days" | "months" | "years" | undefined {
275
+ if (raw === "days" || raw === "months" || raw === "years") return raw;
276
+ return undefined;
277
+ }