@beyondwork/docx-react-component 1.0.28 → 1.0.30
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 +26 -37
- package/src/api/public-types.ts +531 -0
- package/src/api/session-state.ts +2 -0
- package/src/core/commands/index.ts +201 -79
- package/src/core/commands/table-structure-commands.ts +138 -5
- package/src/core/state/text-transaction.ts +370 -3
- package/src/index.ts +41 -0
- package/src/io/docx-session.ts +318 -25
- package/src/io/export/serialize-footnotes.ts +41 -46
- package/src/io/export/serialize-headers-footers.ts +36 -40
- package/src/io/export/serialize-main-document.ts +55 -89
- package/src/io/export/serialize-numbering.ts +104 -4
- package/src/io/export/serialize-runtime-revisions.ts +196 -2
- package/src/io/export/split-story-blocks-for-runtime-revisions.ts +252 -0
- package/src/io/export/table-properties-xml.ts +318 -0
- package/src/io/normalize/normalize-text.ts +34 -3
- package/src/io/ooxml/parse-comments.ts +6 -0
- package/src/io/ooxml/parse-footnotes.ts +69 -13
- package/src/io/ooxml/parse-headers-footers.ts +54 -11
- package/src/io/ooxml/parse-main-document.ts +112 -42
- package/src/io/ooxml/parse-numbering.ts +341 -26
- package/src/io/ooxml/parse-revisions.ts +118 -4
- package/src/io/ooxml/parse-styles.ts +176 -0
- package/src/io/ooxml/parse-tables.ts +34 -25
- package/src/io/ooxml/revision-boundaries.ts +127 -3
- package/src/io/ooxml/workflow-payload.ts +544 -0
- package/src/model/canonical-document.ts +91 -1
- package/src/model/snapshot.ts +112 -1
- package/src/preservation/store.ts +73 -3
- package/src/review/store/comment-store.ts +19 -1
- package/src/review/store/revision-actions.ts +29 -0
- package/src/review/store/revision-store.ts +12 -1
- package/src/review/store/revision-types.ts +11 -0
- package/src/runtime/context-analytics.ts +824 -0
- package/src/runtime/document-locations.ts +521 -0
- package/src/runtime/document-navigation.ts +14 -1
- package/src/runtime/document-outline.ts +440 -0
- package/src/runtime/document-runtime.ts +941 -45
- package/src/runtime/event-refresh-hints.ts +137 -0
- package/src/runtime/numbering-prefix.ts +67 -39
- package/src/runtime/page-layout-estimation.ts +100 -7
- package/src/runtime/resolved-numbering-geometry.ts +293 -0
- package/src/runtime/session-capabilities.ts +2 -2
- package/src/runtime/suggestions-snapshot.ts +137 -0
- package/src/runtime/surface-projection.ts +223 -27
- package/src/runtime/table-style-resolver.ts +409 -0
- package/src/runtime/view-state.ts +17 -1
- package/src/runtime/workflow-markup.ts +54 -14
- package/src/ui/WordReviewEditor.tsx +1269 -87
- package/src/ui/editor-command-bag.ts +7 -0
- package/src/ui/editor-runtime-boundary.ts +111 -10
- package/src/ui/editor-shell-view.tsx +17 -15
- package/src/ui/editor-surface-controller.tsx +5 -0
- package/src/ui/headless/selection-tool-context.ts +19 -0
- package/src/ui/headless/selection-tool-resolver.ts +752 -0
- package/src/ui/headless/selection-tool-types.ts +129 -0
- package/src/ui/headless/selection-toolbar-model.ts +10 -33
- package/src/ui/runtime-shortcut-dispatch.ts +365 -0
- package/src/ui-tailwind/chrome/chrome-preset-model.ts +107 -0
- package/src/ui-tailwind/chrome/chrome-preset-toolbar.tsx +15 -0
- package/src/ui-tailwind/chrome/review-queue-bar.tsx +97 -0
- package/src/ui-tailwind/chrome/tw-context-analytics-summary.tsx +122 -0
- package/src/ui-tailwind/chrome/tw-image-context-toolbar.tsx +1 -9
- package/src/ui-tailwind/chrome/tw-object-context-toolbar.tsx +1 -5
- package/src/ui-tailwind/chrome/tw-page-ruler.tsx +8 -29
- package/src/ui-tailwind/chrome/tw-selection-tool-blocked.tsx +23 -0
- package/src/ui-tailwind/chrome/tw-selection-tool-comment.tsx +35 -0
- package/src/ui-tailwind/chrome/tw-selection-tool-formatting.tsx +37 -0
- package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +298 -0
- package/src/ui-tailwind/chrome/tw-selection-tool-structure.tsx +116 -0
- package/src/ui-tailwind/chrome/tw-selection-tool-suggestion.tsx +29 -0
- package/src/ui-tailwind/chrome/tw-selection-tool-workflow.tsx +27 -0
- package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +3 -3
- package/src/ui-tailwind/chrome/tw-suggestion-card.tsx +3 -3
- package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +86 -14
- package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +57 -52
- package/src/ui-tailwind/editor-surface/pm-decorations.ts +36 -52
- package/src/ui-tailwind/editor-surface/pm-schema.ts +56 -5
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +87 -24
- package/src/ui-tailwind/editor-surface/surface-build-keys.ts +4 -0
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +135 -32
- package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +74 -7
- package/src/ui-tailwind/review/tw-comment-sidebar.tsx +17 -17
- package/src/ui-tailwind/review/tw-review-rail.tsx +19 -17
- package/src/ui-tailwind/review/tw-revision-sidebar.tsx +10 -10
- package/src/ui-tailwind/status/tw-status-bar.tsx +10 -6
- package/src/ui-tailwind/theme/editor-theme.css +58 -40
- package/src/ui-tailwind/toolbar/tw-toolbar-icon-button.tsx +4 -4
- package/src/ui-tailwind/toolbar/tw-toolbar.tsx +250 -181
- package/src/ui-tailwind/tw-review-workspace.tsx +323 -280
- package/src/validation/compatibility-engine.ts +246 -2
- package/src/validation/docx-comment-proof.ts +24 -11
|
@@ -0,0 +1,544 @@
|
|
|
1
|
+
import type { OpcPackage } from "../opc/package-reader.ts";
|
|
2
|
+
import type {
|
|
3
|
+
OpcCompressionMethod,
|
|
4
|
+
OpcRelationship,
|
|
5
|
+
} from "./part-manifest.ts";
|
|
6
|
+
import type {
|
|
7
|
+
WorkflowMetadataDefinition,
|
|
8
|
+
WorkflowMetadataEntry,
|
|
9
|
+
WorkflowMetadataSnapshot,
|
|
10
|
+
} from "../../api/public-types.ts";
|
|
11
|
+
|
|
12
|
+
export const WORKFLOW_PAYLOAD_PART_PATH = "/customXml/item1.xml";
|
|
13
|
+
export const WORKFLOW_PAYLOAD_ITEM_PROPS_PART_PATH = "/customXml/itemProps1.xml";
|
|
14
|
+
export const WORKFLOW_PAYLOAD_CUSTOM_PROPS_PART_PATH = "/docProps/custom.xml";
|
|
15
|
+
export const WORKFLOW_PAYLOAD_CONTENT_TYPE = "application/xml";
|
|
16
|
+
export const WORKFLOW_PAYLOAD_ITEM_PROPS_CONTENT_TYPE =
|
|
17
|
+
"application/vnd.openxmlformats-officedocument.customXmlProperties+xml";
|
|
18
|
+
export const WORKFLOW_PAYLOAD_CUSTOM_PROPS_CONTENT_TYPE =
|
|
19
|
+
"application/vnd.openxmlformats-officedocument.custom-properties+xml";
|
|
20
|
+
export const WORKFLOW_PAYLOAD_ITEM_PROPS_RELATIONSHIP_TYPE =
|
|
21
|
+
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/customXmlProps";
|
|
22
|
+
export const WORKFLOW_PAYLOAD_CUSTOM_PROPS_RELATIONSHIP_TYPE =
|
|
23
|
+
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/custom-properties";
|
|
24
|
+
|
|
25
|
+
type EmbeddedWorkflowPayloadDescriptor = {
|
|
26
|
+
payloadId: string;
|
|
27
|
+
itemId: string;
|
|
28
|
+
documentId: string;
|
|
29
|
+
payloadPartPath: string;
|
|
30
|
+
itemPropsPartPath: string;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export function getDocumentBackedWorkflowMetadata(
|
|
34
|
+
snapshot: WorkflowMetadataSnapshot | undefined,
|
|
35
|
+
): WorkflowMetadataSnapshot {
|
|
36
|
+
if (!snapshot) {
|
|
37
|
+
return { definitions: [], entries: [] };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const definitions = snapshot.definitions.filter(
|
|
41
|
+
(definition) => definition.persistence === "document-metadata",
|
|
42
|
+
);
|
|
43
|
+
const allowedIds = new Set(definitions.map((definition) => definition.metadataId));
|
|
44
|
+
const entries = snapshot.entries.filter((entry) => allowedIds.has(entry.metadataId));
|
|
45
|
+
return {
|
|
46
|
+
definitions,
|
|
47
|
+
entries,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function buildWorkflowPayloadParts(input: {
|
|
52
|
+
sourcePackage: OpcPackage;
|
|
53
|
+
workflowMetadata: WorkflowMetadataSnapshot | undefined;
|
|
54
|
+
documentId: string;
|
|
55
|
+
createdAt: string;
|
|
56
|
+
updatedAt: string;
|
|
57
|
+
producerVersion: string;
|
|
58
|
+
}): {
|
|
59
|
+
payloadPartPath: string;
|
|
60
|
+
itemPropsPartPath: string;
|
|
61
|
+
payloadPartXml: string;
|
|
62
|
+
itemPropsXml: string;
|
|
63
|
+
customPropertiesXml: string;
|
|
64
|
+
payloadRelationships: OpcRelationship[];
|
|
65
|
+
descriptor: EmbeddedWorkflowPayloadDescriptor;
|
|
66
|
+
} | null {
|
|
67
|
+
const workflowMetadata = getDocumentBackedWorkflowMetadata(input.workflowMetadata);
|
|
68
|
+
if (workflowMetadata.definitions.length === 0 && workflowMetadata.entries.length === 0) {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const descriptor = resolveDescriptor(input.sourcePackage, input.documentId);
|
|
73
|
+
const payloadPartXml = buildPayloadXml({
|
|
74
|
+
descriptor,
|
|
75
|
+
createdAt: input.createdAt,
|
|
76
|
+
updatedAt: input.updatedAt,
|
|
77
|
+
producerVersion: input.producerVersion,
|
|
78
|
+
workflowMetadata,
|
|
79
|
+
});
|
|
80
|
+
const itemPropsXml = buildItemPropsXml(descriptor.itemId);
|
|
81
|
+
const customPropertiesXml = buildCustomPropertiesXml(descriptor, workflowMetadata, input.sourcePackage);
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
payloadPartPath: descriptor.payloadPartPath,
|
|
85
|
+
itemPropsPartPath: descriptor.itemPropsPartPath,
|
|
86
|
+
payloadPartXml,
|
|
87
|
+
itemPropsXml,
|
|
88
|
+
customPropertiesXml: buildCustomPropertiesXml(descriptor, workflowMetadata, input.sourcePackage),
|
|
89
|
+
payloadRelationships: [
|
|
90
|
+
{
|
|
91
|
+
id: "rId1",
|
|
92
|
+
type: WORKFLOW_PAYLOAD_ITEM_PROPS_RELATIONSHIP_TYPE,
|
|
93
|
+
target: descriptor.itemPropsPartPath.split("/").pop() ?? "itemProps1.xml",
|
|
94
|
+
targetMode: "internal",
|
|
95
|
+
},
|
|
96
|
+
],
|
|
97
|
+
descriptor,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function parseWorkflowPayloadFromPackage(
|
|
102
|
+
sourcePackage: OpcPackage,
|
|
103
|
+
): WorkflowMetadataSnapshot | undefined {
|
|
104
|
+
const payloadPartPath = resolvePayloadPartPath(sourcePackage);
|
|
105
|
+
if (!payloadPartPath) {
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|
|
108
|
+
const payloadPart = sourcePackage.parts.get(payloadPartPath);
|
|
109
|
+
if (!payloadPart) {
|
|
110
|
+
return undefined;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const xml = decodeUtf8(payloadPart.bytes);
|
|
114
|
+
const entryMatches = [...xml.matchAll(/<bw:entry\b([^>]*)>([\s\S]*?)<\/bw:entry>/g)];
|
|
115
|
+
const definitions: WorkflowMetadataDefinition[] = [];
|
|
116
|
+
const entries: WorkflowMetadataEntry[] = [];
|
|
117
|
+
|
|
118
|
+
for (const match of entryMatches) {
|
|
119
|
+
const attributes = parseAttributes(match[1] ?? "");
|
|
120
|
+
if (!attributes.key) {
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
if (attributes.key.startsWith("definition.")) {
|
|
124
|
+
const metadataId = attributes.key.replace(/^definition\./, "");
|
|
125
|
+
const kind = attributes.ns ?? "tag-category";
|
|
126
|
+
const label = decodeText(match[2] ?? "");
|
|
127
|
+
if (!metadataId || !kind || !label) {
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
definitions.push({
|
|
131
|
+
metadataId,
|
|
132
|
+
kind,
|
|
133
|
+
label,
|
|
134
|
+
...(attributes.source ? { color: attributes.source } : {}),
|
|
135
|
+
...(attributes.status ? { icon: attributes.status } : {}),
|
|
136
|
+
persistence: "document-metadata",
|
|
137
|
+
});
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const payloadText = decodeText(match[2] ?? "");
|
|
142
|
+
const targetKey = attributes.scope === "document"
|
|
143
|
+
? undefined
|
|
144
|
+
: attributes.scope?.startsWith("scope:")
|
|
145
|
+
? attributes.scope.slice("scope:".length)
|
|
146
|
+
: undefined;
|
|
147
|
+
const entryId = attributes.key;
|
|
148
|
+
const metadataId = attributes.metadataId ?? attributes.key;
|
|
149
|
+
if (!entryId || !metadataId) {
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
entries.push({
|
|
153
|
+
entryId,
|
|
154
|
+
metadataId,
|
|
155
|
+
anchor: {
|
|
156
|
+
kind: "range",
|
|
157
|
+
from: 0,
|
|
158
|
+
to: 0,
|
|
159
|
+
assoc: { start: -1, end: 1 },
|
|
160
|
+
},
|
|
161
|
+
storyTarget: parseWorkflowStoryTarget(attributes),
|
|
162
|
+
value: parseWorkflowMetadataValue(payloadText, attributes.valueType),
|
|
163
|
+
...(targetKey ? { scopeId: targetKey } : {}),
|
|
164
|
+
...(attributes.scope?.startsWith("workItem:")
|
|
165
|
+
? { workItemId: attributes.scope.slice("workItem:".length) }
|
|
166
|
+
: {}),
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return {
|
|
171
|
+
definitions,
|
|
172
|
+
entries,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function buildPayloadXml(input: {
|
|
177
|
+
descriptor: EmbeddedWorkflowPayloadDescriptor;
|
|
178
|
+
createdAt: string;
|
|
179
|
+
updatedAt: string;
|
|
180
|
+
producerVersion: string;
|
|
181
|
+
workflowMetadata: WorkflowMetadataSnapshot;
|
|
182
|
+
}): string {
|
|
183
|
+
const definitionEntriesXml = input.workflowMetadata.definitions
|
|
184
|
+
.map((definition) => [
|
|
185
|
+
`<bw:entry`,
|
|
186
|
+
` key="definition.${escapeXml(definition.metadataId)}"`,
|
|
187
|
+
` type="string"`,
|
|
188
|
+
` scope="document"`,
|
|
189
|
+
definition.kind ? ` ns="${escapeXml(definition.kind)}"` : "",
|
|
190
|
+
definition.color ? ` source="${escapeXml(definition.color)}"` : "",
|
|
191
|
+
definition.icon ? ` status="${escapeXml(definition.icon)}"` : "",
|
|
192
|
+
`>${escapeXml(definition.label)}</bw:entry>`,
|
|
193
|
+
].join(""))
|
|
194
|
+
.join("\n");
|
|
195
|
+
|
|
196
|
+
const metadataEntriesXml = input.workflowMetadata.entries
|
|
197
|
+
.map((entry) => {
|
|
198
|
+
const serializedValue = serializeWorkflowMetadataValue(entry.value);
|
|
199
|
+
const storyTargetAttributes = serializeWorkflowStoryTarget(entry.storyTarget);
|
|
200
|
+
|
|
201
|
+
return [
|
|
202
|
+
`<bw:entry`,
|
|
203
|
+
` key="${escapeXml(entry.entryId)}"`,
|
|
204
|
+
` metadataId="${escapeXml(entry.metadataId)}"`,
|
|
205
|
+
` type="string"`,
|
|
206
|
+
` valueType="${escapeXml(serializedValue.type)}"`,
|
|
207
|
+
` scope="${escapeXml(entry.scopeId ? `scope:${entry.scopeId}` : entry.workItemId ? `workItem:${entry.workItemId}` : "document")}"`,
|
|
208
|
+
storyTargetAttributes,
|
|
209
|
+
`>${escapeXml(serializedValue.text)}</bw:entry>`,
|
|
210
|
+
].join("");
|
|
211
|
+
})
|
|
212
|
+
.filter((value) => value.length > 0)
|
|
213
|
+
.join("\n");
|
|
214
|
+
|
|
215
|
+
return [
|
|
216
|
+
`<?xml version="1.0" encoding="UTF-8"?>`,
|
|
217
|
+
`<bw:workflowPayload xmlns:bw="urn:beyondwork:workflow-payload:1" version="1.0" payloadId="${escapeXml(input.descriptor.payloadId)}" itemId="${escapeXml(input.descriptor.itemId)}" documentId="${escapeXml(input.descriptor.documentId)}" createdAt="${escapeXml(input.createdAt)}" updatedAt="${escapeXml(input.updatedAt)}">`,
|
|
218
|
+
` <bw:manifest>`,
|
|
219
|
+
` <bw:producer name="@beyondwork/docx-react-component" version="${escapeXml(input.producerVersion)}" />`,
|
|
220
|
+
` <bw:compatibility rebindMode="best-effort" preserveUnknownExtensions="true" />`,
|
|
221
|
+
` </bw:manifest>`,
|
|
222
|
+
` <bw:metadata>`,
|
|
223
|
+
definitionEntriesXml ? indentLines(definitionEntriesXml, 4) : "",
|
|
224
|
+
metadataEntriesXml ? indentLines(metadataEntriesXml, 4) : "",
|
|
225
|
+
` </bw:metadata>`,
|
|
226
|
+
` <bw:extensions />`,
|
|
227
|
+
`</bw:workflowPayload>`,
|
|
228
|
+
].filter((line) => line.length > 0).join("\n");
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function buildItemPropsXml(itemId: string): string {
|
|
232
|
+
return [
|
|
233
|
+
`<?xml version="1.0" encoding="UTF-8" standalone="yes"?>`,
|
|
234
|
+
`<ds:datastoreItem ds:itemID="${escapeXml(itemId)}" xmlns:ds="http://schemas.openxmlformats.org/officeDocument/2006/customXmlDataProps">`,
|
|
235
|
+
` <ds:schemaRefs />`,
|
|
236
|
+
`</ds:datastoreItem>`,
|
|
237
|
+
].join("\n");
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function buildCustomPropertiesXml(
|
|
241
|
+
descriptor: EmbeddedWorkflowPayloadDescriptor,
|
|
242
|
+
workflowMetadata: WorkflowMetadataSnapshot,
|
|
243
|
+
sourcePackage: OpcPackage,
|
|
244
|
+
): string {
|
|
245
|
+
const existing = parseCustomPropertiesXml(
|
|
246
|
+
sourcePackage.parts.get(WORKFLOW_PAYLOAD_CUSTOM_PROPS_PART_PATH)?.bytes,
|
|
247
|
+
);
|
|
248
|
+
const ownedNames = new Set([
|
|
249
|
+
"BwWorkflowSchemaVersion",
|
|
250
|
+
"BwWorkflowPayloadId",
|
|
251
|
+
"BwWorkflowPayloadItemId",
|
|
252
|
+
"BwWorkflowDocumentId",
|
|
253
|
+
"BwWorkflowMetadataSummary",
|
|
254
|
+
]);
|
|
255
|
+
const preserved = existing.filter((entry) => !ownedNames.has(entry.name));
|
|
256
|
+
const maxPid = existing.reduce((max, entry) => Math.max(max, entry.pid), 1);
|
|
257
|
+
let nextPid = maxPid + 1;
|
|
258
|
+
const owned = [
|
|
259
|
+
{ name: "BwWorkflowSchemaVersion", value: "1.0" },
|
|
260
|
+
{ name: "BwWorkflowPayloadId", value: descriptor.payloadId },
|
|
261
|
+
{ name: "BwWorkflowPayloadItemId", value: descriptor.itemId },
|
|
262
|
+
{ name: "BwWorkflowDocumentId", value: descriptor.documentId },
|
|
263
|
+
{
|
|
264
|
+
name: "BwWorkflowMetadataSummary",
|
|
265
|
+
value: workflowMetadata.definitions.map((definition) => definition.metadataId).join(","),
|
|
266
|
+
},
|
|
267
|
+
].map((entry) => {
|
|
268
|
+
const existingEntry = existing.find((candidate) => candidate.name === entry.name);
|
|
269
|
+
return buildCustomProperty(existingEntry?.pid ?? nextPid++, entry.name, entry.value);
|
|
270
|
+
});
|
|
271
|
+
const preservedXml = preserved.map((entry) => buildCustomProperty(entry.pid, entry.name, entry.value));
|
|
272
|
+
const properties = [...preservedXml, ...owned].join("\n");
|
|
273
|
+
|
|
274
|
+
return [
|
|
275
|
+
`<?xml version="1.0" encoding="UTF-8" standalone="yes"?>`,
|
|
276
|
+
`<Properties xmlns="http://schemas.openxmlformats.org/officeDocument/2006/custom-properties" xmlns:vt="http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes">`,
|
|
277
|
+
properties,
|
|
278
|
+
`</Properties>`,
|
|
279
|
+
].join("\n");
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function buildCustomProperty(pid: number, name: string, value: string): string {
|
|
283
|
+
return ` <property fmtid="{D5CDD505-2E9C-101B-9397-08002B2CF9AE}" pid="${pid}" name="${escapeXml(name)}"><vt:lpwstr>${escapeXml(value)}</vt:lpwstr></property>`;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function serializeWorkflowMetadataValue(
|
|
287
|
+
value: WorkflowMetadataEntry["value"],
|
|
288
|
+
): { type: "json"; text: string } {
|
|
289
|
+
return {
|
|
290
|
+
type: "json",
|
|
291
|
+
text: JSON.stringify(value ?? {}),
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function parseWorkflowMetadataValue(
|
|
296
|
+
payloadText: string,
|
|
297
|
+
valueType: string | undefined,
|
|
298
|
+
): WorkflowMetadataEntry["value"] {
|
|
299
|
+
const decodedText = decodeXmlEntities(payloadText);
|
|
300
|
+
if (!decodedText) {
|
|
301
|
+
return undefined;
|
|
302
|
+
}
|
|
303
|
+
if (!payloadText) {
|
|
304
|
+
return undefined;
|
|
305
|
+
}
|
|
306
|
+
if (valueType === "json") {
|
|
307
|
+
try {
|
|
308
|
+
const parsed = JSON.parse(decodedText) as unknown;
|
|
309
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
310
|
+
return parsed as WorkflowMetadataEntry["value"];
|
|
311
|
+
}
|
|
312
|
+
return { value: parsed };
|
|
313
|
+
} catch {
|
|
314
|
+
return { value: decodedText };
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
if (decodedText.includes("=")) {
|
|
318
|
+
const pairs = decodedText
|
|
319
|
+
.split(";")
|
|
320
|
+
.map((entry) => entry.trim())
|
|
321
|
+
.filter((entry) => entry.length > 0)
|
|
322
|
+
.map((entry) => {
|
|
323
|
+
const separatorIndex = entry.indexOf("=");
|
|
324
|
+
if (separatorIndex <= 0) {
|
|
325
|
+
return null;
|
|
326
|
+
}
|
|
327
|
+
return [
|
|
328
|
+
entry.slice(0, separatorIndex),
|
|
329
|
+
entry.slice(separatorIndex + 1),
|
|
330
|
+
] as const;
|
|
331
|
+
})
|
|
332
|
+
.filter((entry): entry is readonly [string, string] => entry !== null);
|
|
333
|
+
if (pairs.length > 0) {
|
|
334
|
+
return Object.fromEntries(pairs);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
return { value: decodedText };
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function serializeWorkflowStoryTarget(target: WorkflowMetadataEntry["storyTarget"]): string {
|
|
341
|
+
const storyTarget = target ?? { kind: "main" as const };
|
|
342
|
+
switch (storyTarget.kind) {
|
|
343
|
+
case "main":
|
|
344
|
+
return ` storyKind="main"`;
|
|
345
|
+
case "header":
|
|
346
|
+
case "footer":
|
|
347
|
+
return [
|
|
348
|
+
` storyKind="${escapeXml(storyTarget.kind)}"`,
|
|
349
|
+
` storyRelationshipId="${escapeXml(storyTarget.relationshipId)}"`,
|
|
350
|
+
` storyVariant="${escapeXml(storyTarget.variant)}"`,
|
|
351
|
+
typeof storyTarget.sectionIndex === "number"
|
|
352
|
+
? ` storySectionIndex="${String(storyTarget.sectionIndex)}"`
|
|
353
|
+
: "",
|
|
354
|
+
].join("");
|
|
355
|
+
case "footnote":
|
|
356
|
+
case "endnote":
|
|
357
|
+
return [
|
|
358
|
+
` storyKind="${escapeXml(storyTarget.kind)}"`,
|
|
359
|
+
` storyNoteId="${escapeXml(storyTarget.noteId)}"`,
|
|
360
|
+
].join("");
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function parseWorkflowStoryTarget(
|
|
365
|
+
attributes: Record<string, string>,
|
|
366
|
+
): WorkflowMetadataEntry["storyTarget"] {
|
|
367
|
+
const kind = attributes.storyKind;
|
|
368
|
+
switch (kind) {
|
|
369
|
+
case "header":
|
|
370
|
+
case "footer":
|
|
371
|
+
if (!attributes.storyRelationshipId || !attributes.storyVariant) {
|
|
372
|
+
return { kind: "main" };
|
|
373
|
+
}
|
|
374
|
+
return {
|
|
375
|
+
kind,
|
|
376
|
+
relationshipId: attributes.storyRelationshipId,
|
|
377
|
+
variant: attributes.storyVariant as "default" | "first" | "even",
|
|
378
|
+
sectionIndex: attributes.storySectionIndex !== undefined
|
|
379
|
+
? Number(attributes.storySectionIndex)
|
|
380
|
+
: undefined,
|
|
381
|
+
};
|
|
382
|
+
case "footnote":
|
|
383
|
+
case "endnote":
|
|
384
|
+
if (!attributes.storyNoteId) {
|
|
385
|
+
return { kind: "main" };
|
|
386
|
+
}
|
|
387
|
+
return {
|
|
388
|
+
kind,
|
|
389
|
+
noteId: attributes.storyNoteId,
|
|
390
|
+
};
|
|
391
|
+
case "main":
|
|
392
|
+
default:
|
|
393
|
+
return { kind: "main" };
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function resolveDescriptor(sourcePackage: OpcPackage, documentId: string): EmbeddedWorkflowPayloadDescriptor {
|
|
398
|
+
const existingPayloadPartPath = resolvePayloadPartPath(sourcePackage);
|
|
399
|
+
if (existingPayloadPartPath) {
|
|
400
|
+
const payloadPart = sourcePackage.parts.get(existingPayloadPartPath);
|
|
401
|
+
const xml = payloadPart ? decodeUtf8(payloadPart.bytes) : "";
|
|
402
|
+
const attributes = parseAttributes(xml.match(/<bw:workflowPayload\b([^>]*)>/)?.[1] ?? "");
|
|
403
|
+
const payloadId = attributes.payloadId ?? `bw-${simpleHash(documentId)}`;
|
|
404
|
+
const itemId = attributes.itemId ?? createDefaultItemId(documentId);
|
|
405
|
+
const itemNumber = extractItemNumber(existingPayloadPartPath) ?? 1;
|
|
406
|
+
return {
|
|
407
|
+
payloadId,
|
|
408
|
+
itemId,
|
|
409
|
+
documentId,
|
|
410
|
+
payloadPartPath: existingPayloadPartPath,
|
|
411
|
+
itemPropsPartPath: `/customXml/itemProps${itemNumber}.xml`,
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const itemNumber = findNextAvailableItemNumber(sourcePackage);
|
|
416
|
+
const seed = simpleHash(documentId);
|
|
417
|
+
return {
|
|
418
|
+
payloadId: `bw-${seed}`,
|
|
419
|
+
itemId: createDefaultItemId(documentId),
|
|
420
|
+
documentId,
|
|
421
|
+
payloadPartPath: `/customXml/item${itemNumber}.xml`,
|
|
422
|
+
itemPropsPartPath: `/customXml/itemProps${itemNumber}.xml`,
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function resolvePayloadPartPath(sourcePackage: OpcPackage): string | null {
|
|
427
|
+
const mirroredItemId = readCustomPropertyValue(
|
|
428
|
+
sourcePackage,
|
|
429
|
+
"BwWorkflowPayloadItemId",
|
|
430
|
+
);
|
|
431
|
+
const payloadCandidates = [...sourcePackage.parts.keys()]
|
|
432
|
+
.filter((path) => /^\/customXml\/item\d+\.xml$/u.test(path));
|
|
433
|
+
|
|
434
|
+
if (mirroredItemId) {
|
|
435
|
+
const mirroredMatch = payloadCandidates.find((path) => {
|
|
436
|
+
const payloadPart = sourcePackage.parts.get(path);
|
|
437
|
+
const xml = payloadPart ? decodeUtf8(payloadPart.bytes) : "";
|
|
438
|
+
const attributes = parseAttributes(xml.match(/<bw:workflowPayload\b([^>]*)>/)?.[1] ?? "");
|
|
439
|
+
return attributes.itemId === mirroredItemId;
|
|
440
|
+
});
|
|
441
|
+
if (mirroredMatch) {
|
|
442
|
+
return mirroredMatch;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
const direct = payloadCandidates.find((path) => {
|
|
447
|
+
const payloadPart = sourcePackage.parts.get(path);
|
|
448
|
+
const xml = payloadPart ? decodeUtf8(payloadPart.bytes) : "";
|
|
449
|
+
return /<bw:workflowPayload\b/u.test(xml);
|
|
450
|
+
});
|
|
451
|
+
return direct ?? null;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function findNextAvailableItemNumber(sourcePackage: OpcPackage): number {
|
|
455
|
+
const numbers = [...sourcePackage.parts.keys()]
|
|
456
|
+
.map((path) => extractItemNumber(path))
|
|
457
|
+
.filter((value): value is number => typeof value === "number");
|
|
458
|
+
return (numbers.length > 0 ? Math.max(...numbers) : 0) + 1;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function extractItemNumber(path: string): number | null {
|
|
462
|
+
const match = path.match(/^\/customXml\/item(?:Props)?(\d+)\.xml$/u);
|
|
463
|
+
return match ? Number(match[1]) : null;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function createDefaultItemId(documentId: string): string {
|
|
467
|
+
const seed = simpleHash(documentId);
|
|
468
|
+
return `{${seed.slice(0, 8)}-${seed.slice(8, 12)}-${seed.slice(12, 16)}-${seed.slice(16, 20)}-${seed.slice(20, 32)}}`;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function parseCustomPropertiesXml(
|
|
472
|
+
bytes: Uint8Array | undefined,
|
|
473
|
+
): Array<{ pid: number; name: string; value: string }> {
|
|
474
|
+
if (!bytes) {
|
|
475
|
+
return [];
|
|
476
|
+
}
|
|
477
|
+
const xml = decodeUtf8(bytes);
|
|
478
|
+
return [...xml.matchAll(/<property\b([^>]*)>([\s\S]*?)<\/property>/g)].map((match) => {
|
|
479
|
+
const attributes = parseAttributes(match[1] ?? "");
|
|
480
|
+
return {
|
|
481
|
+
pid: Number(attributes.pid ?? "0"),
|
|
482
|
+
name: attributes.name ?? "",
|
|
483
|
+
value: decodeText(match[2] ?? ""),
|
|
484
|
+
};
|
|
485
|
+
}).filter((entry) => entry.pid > 0 && entry.name.length > 0);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function readCustomPropertyValue(sourcePackage: OpcPackage, name: string): string | null {
|
|
489
|
+
return parseCustomPropertiesXml(
|
|
490
|
+
sourcePackage.parts.get(WORKFLOW_PAYLOAD_CUSTOM_PROPS_PART_PATH)?.bytes,
|
|
491
|
+
).find((entry) => entry.name === name)?.value ?? null;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function simpleHash(input: string): string {
|
|
495
|
+
let hash = 2166136261;
|
|
496
|
+
for (const char of input) {
|
|
497
|
+
hash ^= char.charCodeAt(0);
|
|
498
|
+
hash = Math.imul(hash, 16777619);
|
|
499
|
+
}
|
|
500
|
+
const base = Math.abs(hash >>> 0).toString(16).padStart(8, "0");
|
|
501
|
+
return `${base}${base}${base}${base}`;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
function parseAttributes(source: string): Record<string, string> {
|
|
505
|
+
const attributes: Record<string, string> = {};
|
|
506
|
+
for (const match of source.matchAll(/([A-Za-z0-9:_-]+)="([^"]*)"/g)) {
|
|
507
|
+
attributes[match[1] ?? ""] = decodeXmlEntities(match[2] ?? "");
|
|
508
|
+
}
|
|
509
|
+
return attributes;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function indentLines(value: string, spaces: number): string {
|
|
513
|
+
const prefix = " ".repeat(spaces);
|
|
514
|
+
return value.split("\n").map((line) => `${prefix}${line}`).join("\n");
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function decodeUtf8(bytes: Uint8Array): string {
|
|
518
|
+
return new TextDecoder("utf-8").decode(bytes);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
function decodeText(value: string): string {
|
|
522
|
+
return decodeXmlEntities(value)
|
|
523
|
+
.replace(/<[^>]+>/g, "")
|
|
524
|
+
.replace(/\s+/g, " ")
|
|
525
|
+
.trim();
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function decodeXmlEntities(value: string): string {
|
|
529
|
+
return value
|
|
530
|
+
.replace(/"/g, "\"")
|
|
531
|
+
.replace(/'/g, "'")
|
|
532
|
+
.replace(/</g, "<")
|
|
533
|
+
.replace(/>/g, ">")
|
|
534
|
+
.replace(/&/g, "&");
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function escapeXml(value: string): string {
|
|
538
|
+
return value
|
|
539
|
+
.replace(/&/g, "&")
|
|
540
|
+
.replace(/"/g, """)
|
|
541
|
+
.replace(/</g, "<")
|
|
542
|
+
.replace(/>/g, ">")
|
|
543
|
+
.replace(/'/g, "'");
|
|
544
|
+
}
|