@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.
- package/README.md +17 -0
- package/package.json +5 -4
- package/src/api/editor-state-types.ts +110 -0
- package/src/api/public-types.ts +333 -4
- package/src/core/commands/formatting-commands.ts +7 -1
- package/src/core/commands/index.ts +60 -10
- package/src/core/commands/text-commands.ts +59 -0
- package/src/core/search/search-text.ts +15 -2
- package/src/core/selection/review-anchors.ts +131 -21
- package/src/index.ts +29 -1
- package/src/io/chart-preview-resolver.ts +281 -0
- package/src/io/docx-session.ts +692 -2
- package/src/io/export/build-app-properties-xml.ts +1 -1
- package/src/io/export/serialize-comments.ts +38 -9
- package/src/io/export/twip.ts +1 -1
- package/src/io/load-scheduler.ts +230 -0
- package/src/io/normalize/normalize-text.ts +116 -0
- package/src/io/ooxml/parse-comments.ts +0 -33
- package/src/io/ooxml/parse-complex-content.ts +14 -0
- package/src/io/ooxml/parse-main-document.ts +4 -0
- package/src/io/ooxml/workflow-payload-validator.ts +97 -1
- package/src/io/ooxml/workflow-payload.ts +172 -1
- package/src/preservation/opaque-region.ts +5 -0
- package/src/review/store/comment-remapping.ts +2 -2
- package/src/runtime/collab-session.ts +1 -1
- package/src/runtime/document-runtime.ts +661 -42
- package/src/runtime/edit-dispatch/dispatch-text-command.ts +98 -0
- package/src/runtime/edit-dispatch/index.ts +2 -0
- package/src/runtime/edit-dispatch/list-aware-dispatch.ts +125 -0
- package/src/runtime/editor-state-channel.ts +544 -0
- package/src/runtime/editor-state-integration.ts +217 -0
- package/src/runtime/editor-surface/capabilities.ts +411 -0
- package/src/runtime/layout/index.ts +2 -0
- package/src/runtime/layout/inert-layout-facet.ts +4 -0
- package/src/runtime/layout/layout-engine-instance.ts +63 -2
- package/src/runtime/layout/layout-engine-version.ts +41 -0
- package/src/runtime/layout/paginated-layout-engine.ts +211 -14
- package/src/runtime/layout/public-facet.ts +430 -1
- package/src/runtime/perf-counters.ts +28 -0
- package/src/runtime/prerender/cache-envelope.ts +29 -0
- package/src/runtime/prerender/cache-key.ts +66 -0
- package/src/runtime/prerender/font-fingerprint.ts +17 -0
- package/src/runtime/prerender/graph-canonicalize.ts +121 -0
- package/src/runtime/prerender/indexeddb-cache.ts +184 -0
- package/src/runtime/prerender/prerender-document.ts +145 -0
- package/src/runtime/render/block-fragment-projection.ts +2 -0
- package/src/runtime/render/render-frame-types.ts +17 -0
- package/src/runtime/render/render-kernel.ts +172 -29
- package/src/runtime/selection/post-edit-validator.ts +77 -0
- package/src/runtime/surface-projection.ts +45 -7
- package/src/runtime/workflow-markup.ts +71 -16
- package/src/ui/WordReviewEditor.tsx +142 -237
- package/src/ui/editor-command-bag.ts +14 -0
- package/src/ui/editor-runtime-boundary.ts +115 -12
- package/src/ui/editor-shell-view.tsx +10 -0
- package/src/ui/editor-surface-controller.tsx +5 -0
- package/src/ui/headless/selection-helpers.ts +10 -0
- package/src/ui/runtime-shortcut-dispatch.ts +28 -68
- package/src/ui-tailwind/chrome/chrome-preset-toolbar.tsx +62 -2
- package/src/ui-tailwind/chrome/collab-top-nav-container.tsx +281 -0
- package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +48 -0
- package/src/ui-tailwind/editor-surface/paste-plain-text.ts +72 -0
- package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +118 -8
- package/src/ui-tailwind/editor-surface/pm-page-break-decorations.ts +76 -165
- package/src/ui-tailwind/editor-surface/pm-schema.ts +170 -4
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +58 -7
- package/src/ui-tailwind/editor-surface/surface-build-keys.ts +2 -0
- package/src/ui-tailwind/editor-surface/tw-page-block-view.helpers.ts +265 -0
- package/src/ui-tailwind/editor-surface/tw-page-block-view.tsx +8 -255
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +47 -0
- package/src/ui-tailwind/index.ts +5 -1
- package/src/ui-tailwind/page-stack/tw-endnote-area.tsx +57 -0
- package/src/ui-tailwind/page-stack/tw-footnote-area.tsx +71 -0
- package/src/ui-tailwind/page-stack/tw-page-footer-band.tsx +73 -0
- package/src/ui-tailwind/page-stack/tw-page-header-band.tsx +74 -0
- package/src/ui-tailwind/page-stack/tw-page-stack-chrome-layer.tsx +477 -0
- package/src/ui-tailwind/page-stack/tw-region-block-renderer.tsx +374 -0
- package/src/ui-tailwind/page-stack/use-visible-block-range.ts +157 -0
- package/src/ui-tailwind/review/comment-markdown-renderer.tsx +155 -0
- package/src/ui-tailwind/review/tw-comment-sidebar.tsx +77 -16
- package/src/ui-tailwind/theme/editor-theme.css +47 -14
- 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
|
+
}
|