@beyondwork/docx-react-component 1.0.42 → 1.0.45

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 (82) hide show
  1. package/README.md +17 -0
  2. package/package.json +5 -4
  3. package/src/api/editor-state-types.ts +110 -0
  4. package/src/api/public-types.ts +333 -4
  5. package/src/core/commands/formatting-commands.ts +7 -1
  6. package/src/core/commands/index.ts +60 -10
  7. package/src/core/commands/text-commands.ts +59 -0
  8. package/src/core/search/search-text.ts +15 -2
  9. package/src/core/selection/review-anchors.ts +131 -21
  10. package/src/index.ts +29 -1
  11. package/src/io/chart-preview-resolver.ts +281 -0
  12. package/src/io/docx-session.ts +692 -2
  13. package/src/io/export/build-app-properties-xml.ts +1 -1
  14. package/src/io/export/serialize-comments.ts +38 -9
  15. package/src/io/export/twip.ts +1 -1
  16. package/src/io/load-scheduler.ts +230 -0
  17. package/src/io/normalize/normalize-text.ts +116 -0
  18. package/src/io/ooxml/parse-comments.ts +0 -33
  19. package/src/io/ooxml/parse-complex-content.ts +14 -0
  20. package/src/io/ooxml/parse-main-document.ts +4 -0
  21. package/src/io/ooxml/workflow-payload-validator.ts +97 -1
  22. package/src/io/ooxml/workflow-payload.ts +172 -1
  23. package/src/preservation/opaque-region.ts +5 -0
  24. package/src/review/store/comment-remapping.ts +2 -2
  25. package/src/runtime/collab-session.ts +1 -1
  26. package/src/runtime/document-runtime.ts +661 -42
  27. package/src/runtime/edit-dispatch/dispatch-text-command.ts +98 -0
  28. package/src/runtime/edit-dispatch/index.ts +2 -0
  29. package/src/runtime/edit-dispatch/list-aware-dispatch.ts +125 -0
  30. package/src/runtime/editor-state-channel.ts +544 -0
  31. package/src/runtime/editor-state-integration.ts +217 -0
  32. package/src/runtime/editor-surface/capabilities.ts +411 -0
  33. package/src/runtime/layout/index.ts +2 -0
  34. package/src/runtime/layout/inert-layout-facet.ts +4 -0
  35. package/src/runtime/layout/layout-engine-instance.ts +63 -2
  36. package/src/runtime/layout/layout-engine-version.ts +41 -0
  37. package/src/runtime/layout/paginated-layout-engine.ts +211 -14
  38. package/src/runtime/layout/public-facet.ts +430 -1
  39. package/src/runtime/perf-counters.ts +28 -0
  40. package/src/runtime/prerender/cache-envelope.ts +29 -0
  41. package/src/runtime/prerender/cache-key.ts +66 -0
  42. package/src/runtime/prerender/font-fingerprint.ts +17 -0
  43. package/src/runtime/prerender/graph-canonicalize.ts +121 -0
  44. package/src/runtime/prerender/indexeddb-cache.ts +184 -0
  45. package/src/runtime/prerender/prerender-document.ts +145 -0
  46. package/src/runtime/render/block-fragment-projection.ts +2 -0
  47. package/src/runtime/render/render-frame-types.ts +17 -0
  48. package/src/runtime/render/render-kernel.ts +172 -29
  49. package/src/runtime/selection/post-edit-validator.ts +77 -0
  50. package/src/runtime/surface-projection.ts +45 -7
  51. package/src/runtime/workflow-markup.ts +71 -16
  52. package/src/ui/WordReviewEditor.tsx +142 -237
  53. package/src/ui/editor-command-bag.ts +14 -0
  54. package/src/ui/editor-runtime-boundary.ts +115 -12
  55. package/src/ui/editor-shell-view.tsx +10 -0
  56. package/src/ui/editor-surface-controller.tsx +5 -0
  57. package/src/ui/headless/selection-helpers.ts +10 -0
  58. package/src/ui/runtime-shortcut-dispatch.ts +28 -68
  59. package/src/ui-tailwind/chrome/chrome-preset-toolbar.tsx +62 -2
  60. package/src/ui-tailwind/chrome/collab-top-nav-container.tsx +281 -0
  61. package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +48 -0
  62. package/src/ui-tailwind/editor-surface/paste-plain-text.ts +72 -0
  63. package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +118 -8
  64. package/src/ui-tailwind/editor-surface/pm-page-break-decorations.ts +76 -165
  65. package/src/ui-tailwind/editor-surface/pm-schema.ts +170 -4
  66. package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +58 -7
  67. package/src/ui-tailwind/editor-surface/surface-build-keys.ts +2 -0
  68. package/src/ui-tailwind/editor-surface/tw-page-block-view.helpers.ts +265 -0
  69. package/src/ui-tailwind/editor-surface/tw-page-block-view.tsx +8 -255
  70. package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +47 -0
  71. package/src/ui-tailwind/index.ts +5 -1
  72. package/src/ui-tailwind/page-stack/tw-endnote-area.tsx +57 -0
  73. package/src/ui-tailwind/page-stack/tw-footnote-area.tsx +71 -0
  74. package/src/ui-tailwind/page-stack/tw-page-footer-band.tsx +73 -0
  75. package/src/ui-tailwind/page-stack/tw-page-header-band.tsx +74 -0
  76. package/src/ui-tailwind/page-stack/tw-page-stack-chrome-layer.tsx +477 -0
  77. package/src/ui-tailwind/page-stack/tw-region-block-renderer.tsx +374 -0
  78. package/src/ui-tailwind/page-stack/use-visible-block-range.ts +157 -0
  79. package/src/ui-tailwind/review/comment-markdown-renderer.tsx +155 -0
  80. package/src/ui-tailwind/review/tw-comment-sidebar.tsx +77 -16
  81. package/src/ui-tailwind/theme/editor-theme.css +47 -14
  82. package/src/ui-tailwind/tw-review-workspace.tsx +303 -123
@@ -0,0 +1,281 @@
1
+ /**
2
+ * Stage 0B.1 — chart preview resolver.
3
+ *
4
+ * Given a freshly imported `CanonicalDocument` and the source
5
+ * `OpcPackage`, iterate every `chart_preview` inline node whose
6
+ * `previewMediaId` is unset, extract the chart part XML + theme, and
7
+ * ask the host adapter's `renderChartPreview` callback for preview
8
+ * bytes. Successful returns become synthesized `MediaItem` entries on
9
+ * the document; unresolved charts keep their existing shape so
10
+ * downstream rendering falls back to the Stage 0 typed badge.
11
+ *
12
+ * Pure Node-side. No React imports. Not reachable from the edit path
13
+ * (runs at import time only) so §Performance Invariants in CLAUDE.md
14
+ * about synchronous layout reads do not apply.
15
+ *
16
+ * See docs/plans/lane-5-charts.md §2 Task 2.
17
+ */
18
+
19
+ import type {
20
+ BlockNode,
21
+ CanonicalDocument,
22
+ ChartPreviewNode,
23
+ DocumentRootNode,
24
+ InlineNode,
25
+ MediaItem,
26
+ ParagraphNode,
27
+ } from "../model/canonical-document.ts";
28
+ import type {
29
+ ChartPreviewResolveParams,
30
+ EditorHostAdapter,
31
+ } from "../api/public-types.ts";
32
+ import type { OpcPackage } from "./opc/package-reader.ts";
33
+ import { normalizePartPath, resolveRelationshipTarget } from "./ooxml/part-manifest.ts";
34
+
35
+ interface ResolveContext {
36
+ readonly package: OpcPackage;
37
+ readonly adapter: EditorHostAdapter;
38
+ readonly themeXml: string | undefined;
39
+ /** Monotonic counter so we can generate unique media ids across one run. */
40
+ seq: number;
41
+ }
42
+
43
+ interface PendingResolution {
44
+ /** Path from the document root to the chart_preview node, used to surgically replace the node on resolution. */
45
+ readonly pointer: Pointer;
46
+ readonly node: ChartPreviewNode;
47
+ readonly chartPartPath: string;
48
+ readonly chartXml: string;
49
+ readonly widthEmu: number;
50
+ readonly heightEmu: number;
51
+ }
52
+
53
+ type Pointer = Array<number>;
54
+
55
+ export async function resolveChartPreviewsForDocument(
56
+ doc: CanonicalDocument,
57
+ pkg: OpcPackage,
58
+ adapter: EditorHostAdapter | undefined,
59
+ ): Promise<CanonicalDocument> {
60
+ if (!adapter?.renderChartPreview) return doc;
61
+
62
+ const pending = collectUnresolvedChartPreviews(doc, pkg);
63
+ if (pending.length === 0) return doc;
64
+
65
+ const themeXml = extractPartTextFromPackage(pkg, "/word/theme/theme1.xml");
66
+ const renderer = adapter.renderChartPreview;
67
+ const ctx: ResolveContext = { package: pkg, adapter, themeXml, seq: 0 };
68
+
69
+ const resolutions = await Promise.all(
70
+ pending.map(async (entry) => {
71
+ const params: ChartPreviewResolveParams = {
72
+ chartXml: entry.chartXml,
73
+ chartPartPath: entry.chartPartPath,
74
+ themeXml,
75
+ widthEmu: entry.widthEmu,
76
+ heightEmu: entry.heightEmu,
77
+ };
78
+ try {
79
+ const bytes = await renderer(params);
80
+ if (!bytes || bytes.length === 0) return null;
81
+ return { entry, bytes };
82
+ } catch (err) {
83
+ try {
84
+ adapter.logEvent?.({
85
+ type: "chart_preview_render_failed",
86
+ documentId: doc.docId,
87
+ detail: {
88
+ chartPartPath: entry.chartPartPath,
89
+ message: err instanceof Error ? err.message : String(err),
90
+ },
91
+ });
92
+ } catch {
93
+ // Host's logEvent can throw too; swallow silently — one chart failing
94
+ // must never surface as an unhandled rejection during import.
95
+ }
96
+ return null;
97
+ }
98
+ }),
99
+ );
100
+
101
+ const successful = resolutions.filter(
102
+ (r): r is { entry: PendingResolution; bytes: Uint8Array } => r !== null,
103
+ );
104
+ if (successful.length === 0) return doc;
105
+
106
+ void ctx; // resolveContext is reserved for future use (caching, progress events)
107
+ return applyResolutions(doc, successful);
108
+ }
109
+
110
+ /**
111
+ * Walk the whole content tree and yield one PendingResolution per
112
+ * chart_preview node that (a) has no previewMediaId yet and (b) can
113
+ * resolve its chart part from the package. The pointer path lets
114
+ * `applyResolutions` rewrite those exact nodes without touching the
115
+ * surrounding structure.
116
+ */
117
+ function collectUnresolvedChartPreviews(doc: CanonicalDocument, pkg: OpcPackage): PendingResolution[] {
118
+ const out: PendingResolution[] = [];
119
+ const documentRels = collectDocumentPartRelationships(pkg);
120
+
121
+ const paragraphs = doc.content.children;
122
+ for (let i = 0; i < paragraphs.length; i++) {
123
+ const block = paragraphs[i]!;
124
+ if (block.type !== "paragraph") continue;
125
+ const paragraph = block as ParagraphNode;
126
+ for (let j = 0; j < paragraph.children.length; j++) {
127
+ const child = paragraph.children[j];
128
+ if (!child || child.type !== "chart_preview") continue;
129
+ const chartNode = child as ChartPreviewNode;
130
+ if (chartNode.previewMediaId) continue;
131
+ const resolved = resolveChartPart(chartNode, documentRels, pkg);
132
+ if (!resolved) continue;
133
+ out.push({
134
+ pointer: [i, j],
135
+ node: chartNode,
136
+ chartPartPath: resolved.chartPartPath,
137
+ chartXml: resolved.chartXml,
138
+ widthEmu: resolved.widthEmu,
139
+ heightEmu: resolved.heightEmu,
140
+ });
141
+ }
142
+ }
143
+ return out;
144
+ }
145
+
146
+ function collectDocumentPartRelationships(pkg: OpcPackage): Map<string, string> {
147
+ // Only walk /word/document.xml for Stage 0B.1; headers/footers/endnotes
148
+ // carry charts rarely and will be a later iteration.
149
+ const docPart = pkg.parts.get("/word/document.xml");
150
+ if (!docPart?.relationships) return new Map();
151
+ const map = new Map<string, string>();
152
+ for (const rel of docPart.relationships) {
153
+ if (rel.targetMode !== "internal") continue;
154
+ if (!rel.type.endsWith("/chart")) continue;
155
+ const target = resolveRelationshipTarget("/word/document.xml", rel);
156
+ map.set(rel.id, normalizePartPath(target));
157
+ }
158
+ return map;
159
+ }
160
+
161
+ interface ResolvedChartLocation {
162
+ chartPartPath: string;
163
+ chartXml: string;
164
+ widthEmu: number;
165
+ heightEmu: number;
166
+ }
167
+
168
+ function resolveChartPart(
169
+ node: ChartPreviewNode,
170
+ documentRels: Map<string, string>,
171
+ pkg: OpcPackage,
172
+ ): ResolvedChartLocation | null {
173
+ const relId = extractChartRelId(node.rawXml);
174
+ if (!relId) return null;
175
+ const chartPartPath = documentRels.get(relId);
176
+ if (!chartPartPath) return null;
177
+ const chartXml = extractPartTextFromPackage(pkg, chartPartPath);
178
+ if (!chartXml) return null;
179
+ const extent = extractDrawingExtent(node.rawXml);
180
+ return {
181
+ chartPartPath,
182
+ chartXml,
183
+ widthEmu: extent.widthEmu,
184
+ heightEmu: extent.heightEmu,
185
+ };
186
+ }
187
+
188
+ /** Find `<c:chart ... r:id="rIdN"/>` and return the relationship id. */
189
+ function extractChartRelId(rawXml: string): string | null {
190
+ const match = /<c:chart\b[^>]*?r:id="([^"]+)"/.exec(rawXml);
191
+ return match ? match[1]! : null;
192
+ }
193
+
194
+ /** Read `<wp:extent cx="..." cy="..."/>` out of the drawing XML. */
195
+ function extractDrawingExtent(rawXml: string): { widthEmu: number; heightEmu: number } {
196
+ const match = /<wp:extent\b([^/>]*)\/>/.exec(rawXml);
197
+ if (!match) return { widthEmu: 5486400, heightEmu: 3200400 };
198
+ const attrs = match[1] ?? "";
199
+ const cx = /\bcx="(\d+)"/.exec(attrs)?.[1];
200
+ const cy = /\bcy="(\d+)"/.exec(attrs)?.[1];
201
+ return {
202
+ widthEmu: cx ? parseInt(cx, 10) : 5486400,
203
+ heightEmu: cy ? parseInt(cy, 10) : 3200400,
204
+ };
205
+ }
206
+
207
+ function extractPartTextFromPackage(pkg: OpcPackage, path: string): string | undefined {
208
+ const part = pkg.parts.get(path);
209
+ if (!part?.bytes) return undefined;
210
+ try {
211
+ return new TextDecoder("utf-8").decode(part.bytes);
212
+ } catch {
213
+ return undefined;
214
+ }
215
+ }
216
+
217
+ /**
218
+ * Produce a new CanonicalDocument with the resolved chart_preview
219
+ * nodes carrying previewMediaId + corresponding MediaCatalog entries.
220
+ * Never mutates the input doc.
221
+ */
222
+ function applyResolutions(
223
+ doc: CanonicalDocument,
224
+ resolutions: Array<{ entry: PendingResolution; bytes: Uint8Array }>,
225
+ ): CanonicalDocument {
226
+ const newMediaItems: Record<string, MediaItem> = { ...doc.media.items };
227
+ const updates = new Map<string, { previewMediaId: string }>();
228
+
229
+ let seq = 0;
230
+ for (const { entry, bytes } of resolutions) {
231
+ seq += 1;
232
+ const contentType = inferPreviewContentType(bytes);
233
+ const extension = contentType === "image/png" ? "png" : "svg";
234
+ const packagePartName = `/word/media/chart-preview-synth-${seq}.${extension}`;
235
+ const mediaId = `media:word/media/chart-preview-synth-${seq}.${extension}`;
236
+ newMediaItems[mediaId] = {
237
+ mediaId,
238
+ contentType,
239
+ filename: packagePartName.slice(packagePartName.lastIndexOf("/") + 1),
240
+ packagePartName,
241
+ widthEmu: entry.widthEmu,
242
+ heightEmu: entry.heightEmu,
243
+ };
244
+ const pointerKey = entry.pointer.join(",");
245
+ updates.set(pointerKey, { previewMediaId: mediaId });
246
+ }
247
+
248
+ if (updates.size === 0) return doc;
249
+
250
+ // Clone the content tree along pointer paths only — everything else
251
+ // keeps object identity so downstream React memoization stays stable.
252
+ const newParagraphs: BlockNode[] = doc.content.children.slice();
253
+ for (const [pointerKey, update] of updates) {
254
+ const [pi, ci] = pointerKey.split(",").map((s) => parseInt(s, 10)) as [number, number];
255
+ const paragraph = newParagraphs[pi];
256
+ if (!paragraph || paragraph.type !== "paragraph") continue;
257
+ const newChildren: InlineNode[] = (paragraph as ParagraphNode).children.slice();
258
+ const existing = newChildren[ci];
259
+ if (!existing || existing.type !== "chart_preview") continue;
260
+ newChildren[ci] = { ...(existing as ChartPreviewNode), previewMediaId: update.previewMediaId };
261
+ newParagraphs[pi] = { ...(paragraph as ParagraphNode), children: newChildren };
262
+ }
263
+
264
+ const newContent: DocumentRootNode = { ...doc.content, children: newParagraphs };
265
+ const newMedia = { items: newMediaItems };
266
+ return { ...doc, content: newContent, media: newMedia };
267
+ }
268
+
269
+ /**
270
+ * Content-type sniff from the first bytes of the rendered preview.
271
+ * PNG magic is 0x89 0x50 0x4E 0x47; everything else is assumed to be
272
+ * SVG text. Hosts that need a different content type should extend
273
+ * this in a follow-up.
274
+ */
275
+ function inferPreviewContentType(bytes: Uint8Array): string {
276
+ if (bytes.length >= 4 &&
277
+ bytes[0] === 0x89 && bytes[1] === 0x50 && bytes[2] === 0x4e && bytes[3] === 0x47) {
278
+ return "image/png";
279
+ }
280
+ return "image/svg+xml";
281
+ }