@beyondwork/docx-react-component 1.0.41 → 1.0.43
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 +38 -37
- package/src/api/awareness-identity-types.ts +35 -0
- package/src/api/comment-negotiation-types.ts +130 -0
- package/src/api/comment-presentation-types.ts +106 -0
- package/src/api/editor-state-types.ts +110 -0
- package/src/api/external-custody-types.ts +74 -0
- package/src/api/participants-types.ts +18 -0
- package/src/api/public-types.ts +541 -5
- package/src/api/scope-metadata-resolver-types.ts +88 -0
- package/src/core/commands/formatting-commands.ts +1 -1
- package/src/core/commands/index.ts +601 -9
- package/src/core/search/search-text.ts +15 -2
- package/src/index.ts +131 -1
- package/src/io/docx-session.ts +672 -2
- package/src/io/export/escape-xml-attribute.ts +26 -0
- package/src/io/export/external-send.ts +188 -0
- package/src/io/export/serialize-comments.ts +13 -16
- package/src/io/export/serialize-footnotes.ts +17 -24
- package/src/io/export/serialize-headers-footers.ts +17 -24
- package/src/io/export/serialize-main-document.ts +59 -62
- package/src/io/export/serialize-numbering.ts +20 -27
- package/src/io/export/serialize-runtime-revisions.ts +2 -9
- package/src/io/export/serialize-tables.ts +8 -15
- package/src/io/export/table-properties-xml.ts +25 -32
- package/src/io/import/external-reimport.ts +40 -0
- package/src/io/load-scheduler.ts +230 -0
- package/src/io/normalize/normalize-text.ts +83 -0
- package/src/io/ooxml/bw-xml.ts +244 -0
- package/src/io/ooxml/canonicalize-payload.ts +301 -0
- package/src/io/ooxml/comment-negotiation-payload.ts +288 -0
- package/src/io/ooxml/comment-presentation-payload.ts +311 -0
- package/src/io/ooxml/external-custody-payload.ts +102 -0
- package/src/io/ooxml/participants-payload.ts +97 -0
- package/src/io/ooxml/payload-signature.ts +112 -0
- package/src/io/ooxml/workflow-payload-validator.ts +367 -0
- package/src/io/ooxml/workflow-payload.ts +317 -7
- package/src/runtime/awareness-identity.ts +173 -0
- package/src/runtime/collab/event-types.ts +27 -0
- package/src/runtime/collab-session-bridge.ts +157 -0
- package/src/runtime/collab-session-facet.ts +193 -0
- package/src/runtime/collab-session.ts +273 -0
- package/src/runtime/comment-negotiation-sync.ts +91 -0
- package/src/runtime/comment-negotiation.ts +158 -0
- package/src/runtime/comment-presentation.ts +223 -0
- package/src/runtime/document-runtime.ts +639 -124
- package/src/runtime/editor-state-channel.ts +544 -0
- package/src/runtime/editor-state-integration.ts +217 -0
- package/src/runtime/external-send-runtime.ts +117 -0
- package/src/runtime/layout/docx-font-loader.ts +11 -30
- 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 +139 -14
- package/src/runtime/layout/page-graph.ts +79 -7
- package/src/runtime/layout/paginated-layout-engine.ts +441 -48
- package/src/runtime/layout/public-facet.ts +585 -14
- package/src/runtime/layout/table-row-split.ts +316 -0
- package/src/runtime/markdown-sanitizer.ts +132 -0
- package/src/runtime/participants.ts +134 -0
- package/src/runtime/perf-counters.ts +28 -0
- package/src/runtime/render/render-frame-types.ts +17 -0
- package/src/runtime/render/render-kernel.ts +172 -29
- package/src/runtime/resign-payload.ts +120 -0
- package/src/runtime/surface-projection.ts +10 -5
- package/src/runtime/tamper-gate.ts +157 -0
- package/src/runtime/workflow-markup.ts +80 -16
- package/src/runtime/workflow-rail-segments.ts +244 -5
- package/src/ui/WordReviewEditor.tsx +654 -45
- package/src/ui/editor-command-bag.ts +14 -0
- package/src/ui/editor-runtime-boundary.ts +111 -11
- package/src/ui/editor-shell-view.tsx +21 -0
- package/src/ui/editor-surface-controller.tsx +5 -0
- package/src/ui/headless/selection-helpers.ts +10 -0
- package/src/ui-tailwind/chrome/chrome-preset-model.ts +28 -0
- package/src/ui-tailwind/chrome/chrome-preset-toolbar.tsx +62 -2
- package/src/ui-tailwind/chrome/collab-audience-chip.tsx +73 -0
- package/src/ui-tailwind/chrome/collab-negotiation-action-bar.tsx +244 -0
- package/src/ui-tailwind/chrome/collab-presence-strip.tsx +150 -0
- package/src/ui-tailwind/chrome/collab-role-chip.tsx +62 -0
- package/src/ui-tailwind/chrome/collab-send-to-supplier-button.tsx +68 -0
- package/src/ui-tailwind/chrome/collab-send-to-supplier-modal.tsx +149 -0
- package/src/ui-tailwind/chrome/collab-tamper-banner.tsx +68 -0
- package/src/ui-tailwind/chrome/collab-top-nav-container.tsx +281 -0
- package/src/ui-tailwind/chrome/forward-non-drag-click.ts +104 -0
- package/src/ui-tailwind/chrome/tw-mode-dock.tsx +1 -0
- package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +7 -1
- package/src/ui-tailwind/chrome/tw-table-grip-layer.tsx +1 -38
- package/src/ui-tailwind/chrome-overlay/index.ts +6 -0
- package/src/ui-tailwind/chrome-overlay/scope-card-role-model.ts +78 -0
- package/src/ui-tailwind/chrome-overlay/scope-keyboard-cycle.ts +49 -0
- package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +106 -0
- package/src/ui-tailwind/chrome-overlay/tw-page-stack-overlay-layer.tsx +527 -0
- package/src/ui-tailwind/chrome-overlay/tw-scope-card-layer.tsx +120 -22
- package/src/ui-tailwind/chrome-overlay/tw-scope-card.tsx +310 -32
- package/src/ui-tailwind/chrome-overlay/tw-scope-rail-layer.tsx +93 -14
- 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-decorations.ts +35 -1
- package/src/ui-tailwind/editor-surface/pm-page-break-decorations.ts +86 -3
- package/src/ui-tailwind/editor-surface/pm-schema.ts +167 -17
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +35 -7
- package/src/ui-tailwind/editor-surface/remote-cursor-plugin.ts +20 -3
- 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 +10 -256
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +9 -0
- package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +66 -0
- package/src/ui-tailwind/index.ts +37 -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/review/comment-markdown-renderer.tsx +155 -0
- package/src/ui-tailwind/review/tw-comment-sidebar.tsx +77 -16
- package/src/ui-tailwind/review/tw-review-rail-footer.tsx +29 -3
- package/src/ui-tailwind/status/tw-status-bar.tsx +52 -1
- package/src/ui-tailwind/theme/editor-theme.css +25 -0
- package/src/ui-tailwind/tw-review-workspace.tsx +455 -118
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CommentNegotiationActionType,
|
|
3
|
+
CommentNegotiationEntry,
|
|
4
|
+
CommentNegotiationSnapshot,
|
|
5
|
+
CommentNegotiationState,
|
|
6
|
+
NegotiationCounterProposal,
|
|
7
|
+
NegotiationHistoryRow,
|
|
8
|
+
NegotiationVote,
|
|
9
|
+
} from "../../api/comment-negotiation-types.ts";
|
|
10
|
+
import {
|
|
11
|
+
attrNumber,
|
|
12
|
+
childrenOf,
|
|
13
|
+
firstChild,
|
|
14
|
+
parseBwXml,
|
|
15
|
+
renderElement,
|
|
16
|
+
renderText,
|
|
17
|
+
stripNs,
|
|
18
|
+
textOf,
|
|
19
|
+
} from "./bw-xml.ts";
|
|
20
|
+
|
|
21
|
+
const NS_URI = "urn:beyondwork:workflow-payload:1";
|
|
22
|
+
|
|
23
|
+
const STATE_VOCAB = new Set<CommentNegotiationState>([
|
|
24
|
+
"proposed",
|
|
25
|
+
"negotiating",
|
|
26
|
+
"accepted",
|
|
27
|
+
"rejected",
|
|
28
|
+
"resolved",
|
|
29
|
+
]);
|
|
30
|
+
const VERDICT_VOCAB = new Set<NegotiationVote["verdict"]>([
|
|
31
|
+
"approve",
|
|
32
|
+
"reject",
|
|
33
|
+
"abstain",
|
|
34
|
+
]);
|
|
35
|
+
const ACTION_VOCAB = new Set<CommentNegotiationActionType>([
|
|
36
|
+
"propose-change",
|
|
37
|
+
"counter-propose",
|
|
38
|
+
"vote",
|
|
39
|
+
"accept",
|
|
40
|
+
"reject",
|
|
41
|
+
"lock",
|
|
42
|
+
"reopen",
|
|
43
|
+
]);
|
|
44
|
+
const EDIT_KIND_VOCAB = new Set<"replace" | "insert" | "delete">([
|
|
45
|
+
"replace",
|
|
46
|
+
"insert",
|
|
47
|
+
"delete",
|
|
48
|
+
]);
|
|
49
|
+
|
|
50
|
+
export function buildCommentNegotiationXml(
|
|
51
|
+
snap: CommentNegotiationSnapshot,
|
|
52
|
+
): string {
|
|
53
|
+
const threads = snap.entries.map(buildThread).join("");
|
|
54
|
+
return renderElement(
|
|
55
|
+
"bw:commentNegotiation",
|
|
56
|
+
{
|
|
57
|
+
"xmlns:bw": NS_URI,
|
|
58
|
+
schemaVersion: String(snap.schemaVersion),
|
|
59
|
+
},
|
|
60
|
+
[threads],
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function buildThread(entry: CommentNegotiationEntry): string {
|
|
65
|
+
const approvers = entry.requiredApprovers.length
|
|
66
|
+
? renderElement("bw:requiredApprovers", {}, [
|
|
67
|
+
entry.requiredApprovers
|
|
68
|
+
.map((id) => renderElement("bw:userRef", { id }))
|
|
69
|
+
.join(""),
|
|
70
|
+
])
|
|
71
|
+
: "";
|
|
72
|
+
|
|
73
|
+
const votes = entry.votes.length
|
|
74
|
+
? renderElement("bw:votes", {}, [
|
|
75
|
+
entry.votes
|
|
76
|
+
.map((v) =>
|
|
77
|
+
renderElement("bw:vote", {
|
|
78
|
+
authorId: v.authorId,
|
|
79
|
+
verdict: v.verdict,
|
|
80
|
+
castAt: v.castAt,
|
|
81
|
+
}),
|
|
82
|
+
)
|
|
83
|
+
.join(""),
|
|
84
|
+
])
|
|
85
|
+
: "";
|
|
86
|
+
|
|
87
|
+
const proposals = entry.counterProposals.length
|
|
88
|
+
? renderElement("bw:counterProposals", {}, [
|
|
89
|
+
entry.counterProposals.map(buildCounterProposal).join(""),
|
|
90
|
+
])
|
|
91
|
+
: "";
|
|
92
|
+
|
|
93
|
+
const history = entry.history.length
|
|
94
|
+
? renderElement("bw:history", {}, [
|
|
95
|
+
entry.history
|
|
96
|
+
.map((h) =>
|
|
97
|
+
renderElement("bw:transition", {
|
|
98
|
+
from: h.from,
|
|
99
|
+
to: h.to,
|
|
100
|
+
actorId: h.actorId,
|
|
101
|
+
at: h.at,
|
|
102
|
+
action: h.action,
|
|
103
|
+
reasonCode: h.reasonCode,
|
|
104
|
+
}),
|
|
105
|
+
)
|
|
106
|
+
.join(""),
|
|
107
|
+
])
|
|
108
|
+
: "";
|
|
109
|
+
|
|
110
|
+
return renderElement(
|
|
111
|
+
"bw:thread",
|
|
112
|
+
{
|
|
113
|
+
commentId: entry.commentId,
|
|
114
|
+
state: entry.state,
|
|
115
|
+
lockedAt: entry.lockedAt,
|
|
116
|
+
lockedBy: entry.lockedBy,
|
|
117
|
+
acceptedProposalId: entry.acceptedProposalId,
|
|
118
|
+
},
|
|
119
|
+
[approvers, votes, proposals, history],
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function buildCounterProposal(p: NegotiationCounterProposal): string {
|
|
124
|
+
const bodyEl = renderElement("bw:body", {}, [renderText(p.body)]);
|
|
125
|
+
const edit = p.proposedRangeEdit
|
|
126
|
+
? renderElement(
|
|
127
|
+
"bw:proposedRangeEdit",
|
|
128
|
+
{
|
|
129
|
+
kind: p.proposedRangeEdit.kind,
|
|
130
|
+
start: String(p.proposedRangeEdit.start),
|
|
131
|
+
end: String(p.proposedRangeEdit.end),
|
|
132
|
+
},
|
|
133
|
+
[p.proposedRangeEdit.text ? renderText(p.proposedRangeEdit.text) : ""],
|
|
134
|
+
)
|
|
135
|
+
: "";
|
|
136
|
+
return renderElement(
|
|
137
|
+
"bw:counterProposal",
|
|
138
|
+
{
|
|
139
|
+
id: p.id,
|
|
140
|
+
authorId: p.authorId,
|
|
141
|
+
createdAt: p.createdAt,
|
|
142
|
+
supersededBy: p.supersededBy,
|
|
143
|
+
},
|
|
144
|
+
[bodyEl, edit],
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function parseCommentNegotiationXml(
|
|
149
|
+
xml: string,
|
|
150
|
+
): CommentNegotiationSnapshot {
|
|
151
|
+
const root = parseBwXml(xml);
|
|
152
|
+
if (stripNs(root.name) !== "commentNegotiation") {
|
|
153
|
+
throw new Error(
|
|
154
|
+
`parseCommentNegotiationXml: expected <bw:commentNegotiation>, got <${root.name}>`,
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
const schemaVersion =
|
|
158
|
+
attrNumber(root.attributes["schemaVersion"]) ?? 1;
|
|
159
|
+
if (schemaVersion !== 1) {
|
|
160
|
+
// Unknown major — preserve-only; return an empty snapshot so the runtime
|
|
161
|
+
// still works. The full tree is preserved by the workflow-payload emitter.
|
|
162
|
+
return { schemaVersion: 1, entries: [] };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const entries: CommentNegotiationEntry[] = [];
|
|
166
|
+
for (const thread of childrenOf(root, "thread")) {
|
|
167
|
+
const state = thread.attributes["state"] as CommentNegotiationState;
|
|
168
|
+
if (!STATE_VOCAB.has(state)) continue;
|
|
169
|
+
|
|
170
|
+
const entry: CommentNegotiationEntry = {
|
|
171
|
+
commentId: thread.attributes["commentId"] ?? "",
|
|
172
|
+
state,
|
|
173
|
+
requiredApprovers: parseApprovers(thread),
|
|
174
|
+
votes: parseVotes(thread),
|
|
175
|
+
counterProposals: parseProposals(thread),
|
|
176
|
+
history: parseHistory(thread),
|
|
177
|
+
};
|
|
178
|
+
if (thread.attributes["lockedAt"] !== undefined) {
|
|
179
|
+
entry.lockedAt = thread.attributes["lockedAt"];
|
|
180
|
+
}
|
|
181
|
+
if (thread.attributes["lockedBy"] !== undefined) {
|
|
182
|
+
entry.lockedBy = thread.attributes["lockedBy"];
|
|
183
|
+
}
|
|
184
|
+
if (thread.attributes["acceptedProposalId"] !== undefined) {
|
|
185
|
+
entry.acceptedProposalId = thread.attributes["acceptedProposalId"];
|
|
186
|
+
}
|
|
187
|
+
entries.push(entry);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return { schemaVersion: 1, entries };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function parseApprovers(
|
|
194
|
+
thread: ReturnType<typeof parseBwXml>,
|
|
195
|
+
): string[] {
|
|
196
|
+
const container = firstChild(thread, "requiredApprovers");
|
|
197
|
+
if (!container) return [];
|
|
198
|
+
return childrenOf(container, "userRef")
|
|
199
|
+
.map((el) => el.attributes["id"])
|
|
200
|
+
.filter((id): id is string => id !== undefined && id !== "");
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function parseVotes(thread: ReturnType<typeof parseBwXml>): NegotiationVote[] {
|
|
204
|
+
const container = firstChild(thread, "votes");
|
|
205
|
+
if (!container) return [];
|
|
206
|
+
const out: NegotiationVote[] = [];
|
|
207
|
+
for (const el of childrenOf(container, "vote")) {
|
|
208
|
+
const verdict = el.attributes["verdict"] as NegotiationVote["verdict"];
|
|
209
|
+
if (!VERDICT_VOCAB.has(verdict)) continue;
|
|
210
|
+
const authorId = el.attributes["authorId"];
|
|
211
|
+
const castAt = el.attributes["castAt"];
|
|
212
|
+
if (!authorId || !castAt) continue;
|
|
213
|
+
out.push({ authorId, verdict, castAt });
|
|
214
|
+
}
|
|
215
|
+
return out;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function parseProposals(
|
|
219
|
+
thread: ReturnType<typeof parseBwXml>,
|
|
220
|
+
): NegotiationCounterProposal[] {
|
|
221
|
+
const container = firstChild(thread, "counterProposals");
|
|
222
|
+
if (!container) return [];
|
|
223
|
+
const out: NegotiationCounterProposal[] = [];
|
|
224
|
+
for (const el of childrenOf(container, "counterProposal")) {
|
|
225
|
+
const id = el.attributes["id"];
|
|
226
|
+
const authorId = el.attributes["authorId"];
|
|
227
|
+
const createdAt = el.attributes["createdAt"];
|
|
228
|
+
if (!id || !authorId || !createdAt) continue;
|
|
229
|
+
const bodyEl = firstChild(el, "body");
|
|
230
|
+
const editEl = firstChild(el, "proposedRangeEdit");
|
|
231
|
+
const proposal: NegotiationCounterProposal = {
|
|
232
|
+
id,
|
|
233
|
+
authorId,
|
|
234
|
+
createdAt,
|
|
235
|
+
body: bodyEl ? textOf(bodyEl) : "",
|
|
236
|
+
};
|
|
237
|
+
if (el.attributes["supersededBy"] !== undefined) {
|
|
238
|
+
proposal.supersededBy = el.attributes["supersededBy"];
|
|
239
|
+
}
|
|
240
|
+
if (editEl) {
|
|
241
|
+
const kind = editEl.attributes["kind"] as "replace" | "insert" | "delete";
|
|
242
|
+
const start = attrNumber(editEl.attributes["start"]);
|
|
243
|
+
const end = attrNumber(editEl.attributes["end"]);
|
|
244
|
+
if (EDIT_KIND_VOCAB.has(kind) && start !== undefined && end !== undefined) {
|
|
245
|
+
const edit: NegotiationCounterProposal["proposedRangeEdit"] = {
|
|
246
|
+
kind,
|
|
247
|
+
start,
|
|
248
|
+
end,
|
|
249
|
+
};
|
|
250
|
+
const text = textOf(editEl);
|
|
251
|
+
if (text.length > 0) edit.text = text;
|
|
252
|
+
proposal.proposedRangeEdit = edit;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
out.push(proposal);
|
|
256
|
+
}
|
|
257
|
+
return out;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function parseHistory(
|
|
261
|
+
thread: ReturnType<typeof parseBwXml>,
|
|
262
|
+
): NegotiationHistoryRow[] {
|
|
263
|
+
const container = firstChild(thread, "history");
|
|
264
|
+
if (!container) return [];
|
|
265
|
+
const out: NegotiationHistoryRow[] = [];
|
|
266
|
+
for (const el of childrenOf(container, "transition")) {
|
|
267
|
+
const from = el.attributes["from"] as CommentNegotiationState;
|
|
268
|
+
const to = el.attributes["to"] as CommentNegotiationState;
|
|
269
|
+
const action = el.attributes["action"] as CommentNegotiationActionType;
|
|
270
|
+
const actorId = el.attributes["actorId"];
|
|
271
|
+
const at = el.attributes["at"];
|
|
272
|
+
if (
|
|
273
|
+
!STATE_VOCAB.has(from) ||
|
|
274
|
+
!STATE_VOCAB.has(to) ||
|
|
275
|
+
!ACTION_VOCAB.has(action) ||
|
|
276
|
+
!actorId ||
|
|
277
|
+
!at
|
|
278
|
+
) {
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
const row: NegotiationHistoryRow = { from, to, actorId, at, action };
|
|
282
|
+
if (el.attributes["reasonCode"] !== undefined) {
|
|
283
|
+
row.reasonCode = el.attributes["reasonCode"];
|
|
284
|
+
}
|
|
285
|
+
out.push(row);
|
|
286
|
+
}
|
|
287
|
+
return out;
|
|
288
|
+
}
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CommentAttachment,
|
|
3
|
+
CommentAudience,
|
|
4
|
+
CommentBody,
|
|
5
|
+
CommentLabel,
|
|
6
|
+
CommentMention,
|
|
7
|
+
CommentPresentation,
|
|
8
|
+
CommentPresentationReply,
|
|
9
|
+
CommentPresentationSnapshot,
|
|
10
|
+
CommentReaction,
|
|
11
|
+
} from "../../api/comment-presentation-types.ts";
|
|
12
|
+
import {
|
|
13
|
+
attrNumber,
|
|
14
|
+
childrenOf,
|
|
15
|
+
firstChild,
|
|
16
|
+
parseBwXml,
|
|
17
|
+
renderCdata,
|
|
18
|
+
renderElement,
|
|
19
|
+
renderText,
|
|
20
|
+
stripNs,
|
|
21
|
+
textOf,
|
|
22
|
+
} from "./bw-xml.ts";
|
|
23
|
+
|
|
24
|
+
const NS_URI = "urn:beyondwork:workflow-payload:1";
|
|
25
|
+
|
|
26
|
+
const AUDIENCE_VOCAB = new Set<CommentAudience>([
|
|
27
|
+
"internal",
|
|
28
|
+
"external",
|
|
29
|
+
"shared",
|
|
30
|
+
]);
|
|
31
|
+
const ATTACHMENT_KIND_VOCAB = new Set<CommentAttachment["kind"]>([
|
|
32
|
+
"image",
|
|
33
|
+
"file",
|
|
34
|
+
"link",
|
|
35
|
+
]);
|
|
36
|
+
|
|
37
|
+
export function buildCommentPresentationXml(
|
|
38
|
+
snap: CommentPresentationSnapshot,
|
|
39
|
+
): string {
|
|
40
|
+
const entries = snap.entries.map(buildComment).join("");
|
|
41
|
+
return renderElement(
|
|
42
|
+
"bw:commentPresentation",
|
|
43
|
+
{
|
|
44
|
+
"xmlns:bw": NS_URI,
|
|
45
|
+
schemaVersion: String(snap.schemaVersion),
|
|
46
|
+
},
|
|
47
|
+
[entries],
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function buildComment(entry: CommentPresentation): string {
|
|
52
|
+
const body = buildBody("bw:body", entry.body);
|
|
53
|
+
const replies = entry.replies.length
|
|
54
|
+
? renderElement("bw:replies", {}, [
|
|
55
|
+
entry.replies.map(buildReply).join(""),
|
|
56
|
+
])
|
|
57
|
+
: "";
|
|
58
|
+
const mentions = entry.mentions.length
|
|
59
|
+
? renderElement("bw:mentions", {}, [
|
|
60
|
+
entry.mentions
|
|
61
|
+
.map((m) =>
|
|
62
|
+
renderElement("bw:mention", {
|
|
63
|
+
userId: m.userId,
|
|
64
|
+
displayName: m.displayName,
|
|
65
|
+
offsetInBody: String(m.offsetInBody),
|
|
66
|
+
entryId: m.entryId,
|
|
67
|
+
}),
|
|
68
|
+
)
|
|
69
|
+
.join(""),
|
|
70
|
+
])
|
|
71
|
+
: "";
|
|
72
|
+
const attachments = entry.attachments.length
|
|
73
|
+
? renderElement("bw:attachments", {}, [
|
|
74
|
+
entry.attachments.map(buildAttachment).join(""),
|
|
75
|
+
])
|
|
76
|
+
: "";
|
|
77
|
+
const reactions = entry.reactions.length
|
|
78
|
+
? renderElement("bw:reactions", {}, [
|
|
79
|
+
entry.reactions
|
|
80
|
+
.map((r) =>
|
|
81
|
+
renderElement("bw:reaction", {
|
|
82
|
+
emoji: r.emoji,
|
|
83
|
+
authorId: r.authorId,
|
|
84
|
+
reactedAt: r.reactedAt,
|
|
85
|
+
}),
|
|
86
|
+
)
|
|
87
|
+
.join(""),
|
|
88
|
+
])
|
|
89
|
+
: "";
|
|
90
|
+
const labels = entry.labels.length
|
|
91
|
+
? renderElement("bw:labels", {}, [
|
|
92
|
+
entry.labels
|
|
93
|
+
.map((l) =>
|
|
94
|
+
renderElement("bw:label", { key: l.key, color: l.color }, [
|
|
95
|
+
renderText(l.text),
|
|
96
|
+
]),
|
|
97
|
+
)
|
|
98
|
+
.join(""),
|
|
99
|
+
])
|
|
100
|
+
: "";
|
|
101
|
+
|
|
102
|
+
return renderElement(
|
|
103
|
+
"bw:comment",
|
|
104
|
+
{
|
|
105
|
+
commentId: entry.commentId,
|
|
106
|
+
audience: entry.audience,
|
|
107
|
+
},
|
|
108
|
+
[body, replies, mentions, attachments, reactions, labels],
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function buildReply(reply: CommentPresentationReply): string {
|
|
113
|
+
return renderElement("bw:reply", { entryId: reply.entryId }, [
|
|
114
|
+
buildBody("bw:body", reply.body),
|
|
115
|
+
]);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function buildBody(elementName: string, body: CommentBody): string {
|
|
119
|
+
const needsCdata = /[<>&]/.test(body.text);
|
|
120
|
+
return renderElement(
|
|
121
|
+
elementName,
|
|
122
|
+
{
|
|
123
|
+
format: body.format,
|
|
124
|
+
digest: body.digest,
|
|
125
|
+
sanitized: body.sanitized ? "true" : undefined,
|
|
126
|
+
},
|
|
127
|
+
[needsCdata ? renderCdata(body.text) : renderText(body.text)],
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function buildAttachment(a: CommentAttachment): string {
|
|
132
|
+
return renderElement("bw:attachment", {
|
|
133
|
+
id: a.id,
|
|
134
|
+
kind: a.kind,
|
|
135
|
+
displayName: a.displayName,
|
|
136
|
+
mimeType: a.mimeType,
|
|
137
|
+
relationshipId: a.relationshipId,
|
|
138
|
+
href: a.href,
|
|
139
|
+
byteLength: a.byteLength !== undefined ? String(a.byteLength) : undefined,
|
|
140
|
+
width: a.width !== undefined ? String(a.width) : undefined,
|
|
141
|
+
height: a.height !== undefined ? String(a.height) : undefined,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function parseCommentPresentationXml(
|
|
146
|
+
xml: string,
|
|
147
|
+
): CommentPresentationSnapshot {
|
|
148
|
+
const root = parseBwXml(xml);
|
|
149
|
+
if (stripNs(root.name) !== "commentPresentation") {
|
|
150
|
+
throw new Error(
|
|
151
|
+
`parseCommentPresentationXml: expected <bw:commentPresentation>, got <${root.name}>`,
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
const schemaVersion = attrNumber(root.attributes["schemaVersion"]) ?? 1;
|
|
155
|
+
if (schemaVersion !== 1) {
|
|
156
|
+
return { schemaVersion: 1, entries: [] };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const entries: CommentPresentation[] = [];
|
|
160
|
+
for (const commentEl of childrenOf(root, "comment")) {
|
|
161
|
+
const commentId = commentEl.attributes["commentId"] ?? "";
|
|
162
|
+
if (!commentId) continue;
|
|
163
|
+
const audienceAttr = commentEl.attributes["audience"] as CommentAudience;
|
|
164
|
+
const audience: CommentAudience = AUDIENCE_VOCAB.has(audienceAttr)
|
|
165
|
+
? audienceAttr
|
|
166
|
+
: "internal"; // fail-closed per schema
|
|
167
|
+
const bodyEl = firstChild(commentEl, "body");
|
|
168
|
+
const body: CommentBody = bodyEl ? parseBody(bodyEl) : emptyBody();
|
|
169
|
+
|
|
170
|
+
entries.push({
|
|
171
|
+
commentId,
|
|
172
|
+
audience,
|
|
173
|
+
body,
|
|
174
|
+
replies: parseReplies(commentEl),
|
|
175
|
+
mentions: parseMentions(commentEl),
|
|
176
|
+
attachments: parseAttachments(commentEl),
|
|
177
|
+
reactions: parseReactions(commentEl),
|
|
178
|
+
labels: parseLabels(commentEl),
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return { schemaVersion: 1, entries };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function parseBody(el: ReturnType<typeof parseBwXml>): CommentBody {
|
|
186
|
+
const body: CommentBody = {
|
|
187
|
+
format: "markdown",
|
|
188
|
+
text: textOf(el),
|
|
189
|
+
digest: el.attributes["digest"] ?? "",
|
|
190
|
+
};
|
|
191
|
+
if (el.attributes["sanitized"] === "true") body.sanitized = true;
|
|
192
|
+
return body;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function emptyBody(): CommentBody {
|
|
196
|
+
return {
|
|
197
|
+
format: "markdown",
|
|
198
|
+
text: "",
|
|
199
|
+
digest: "",
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function parseReplies(
|
|
204
|
+
commentEl: ReturnType<typeof parseBwXml>,
|
|
205
|
+
): CommentPresentationReply[] {
|
|
206
|
+
const container = firstChild(commentEl, "replies");
|
|
207
|
+
if (!container) return [];
|
|
208
|
+
const out: CommentPresentationReply[] = [];
|
|
209
|
+
for (const el of childrenOf(container, "reply")) {
|
|
210
|
+
const entryId = el.attributes["entryId"];
|
|
211
|
+
if (!entryId) continue;
|
|
212
|
+
const bodyEl = firstChild(el, "body");
|
|
213
|
+
out.push({
|
|
214
|
+
entryId,
|
|
215
|
+
body: bodyEl ? parseBody(bodyEl) : emptyBody(),
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
return out;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function parseMentions(
|
|
222
|
+
commentEl: ReturnType<typeof parseBwXml>,
|
|
223
|
+
): CommentMention[] {
|
|
224
|
+
const container = firstChild(commentEl, "mentions");
|
|
225
|
+
if (!container) return [];
|
|
226
|
+
const out: CommentMention[] = [];
|
|
227
|
+
for (const el of childrenOf(container, "mention")) {
|
|
228
|
+
const userId = el.attributes["userId"];
|
|
229
|
+
const displayName = el.attributes["displayName"];
|
|
230
|
+
const offsetInBody = attrNumber(el.attributes["offsetInBody"]);
|
|
231
|
+
if (!userId || !displayName || offsetInBody === undefined) continue;
|
|
232
|
+
const mention: CommentMention = { userId, displayName, offsetInBody };
|
|
233
|
+
if (el.attributes["entryId"] !== undefined) {
|
|
234
|
+
mention.entryId = el.attributes["entryId"];
|
|
235
|
+
}
|
|
236
|
+
out.push(mention);
|
|
237
|
+
}
|
|
238
|
+
return out;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function parseAttachments(
|
|
242
|
+
commentEl: ReturnType<typeof parseBwXml>,
|
|
243
|
+
): CommentAttachment[] {
|
|
244
|
+
const container = firstChild(commentEl, "attachments");
|
|
245
|
+
if (!container) return [];
|
|
246
|
+
const out: CommentAttachment[] = [];
|
|
247
|
+
for (const el of childrenOf(container, "attachment")) {
|
|
248
|
+
const id = el.attributes["id"];
|
|
249
|
+
const kind = el.attributes["kind"] as CommentAttachment["kind"];
|
|
250
|
+
const displayName = el.attributes["displayName"];
|
|
251
|
+
if (!id || !displayName || !ATTACHMENT_KIND_VOCAB.has(kind)) continue;
|
|
252
|
+
const a: CommentAttachment = { id, kind, displayName };
|
|
253
|
+
if (el.attributes["mimeType"] !== undefined) {
|
|
254
|
+
a.mimeType = el.attributes["mimeType"];
|
|
255
|
+
}
|
|
256
|
+
if (el.attributes["relationshipId"] !== undefined) {
|
|
257
|
+
a.relationshipId = el.attributes["relationshipId"];
|
|
258
|
+
}
|
|
259
|
+
if (el.attributes["href"] !== undefined && isAllowedHref(el.attributes["href"])) {
|
|
260
|
+
a.href = el.attributes["href"];
|
|
261
|
+
}
|
|
262
|
+
const byteLength = attrNumber(el.attributes["byteLength"]);
|
|
263
|
+
if (byteLength !== undefined) a.byteLength = byteLength;
|
|
264
|
+
const width = attrNumber(el.attributes["width"]);
|
|
265
|
+
if (width !== undefined) a.width = width;
|
|
266
|
+
const height = attrNumber(el.attributes["height"]);
|
|
267
|
+
if (height !== undefined) a.height = height;
|
|
268
|
+
out.push(a);
|
|
269
|
+
}
|
|
270
|
+
return out;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function isAllowedHref(href: string): boolean {
|
|
274
|
+
return (
|
|
275
|
+
href.startsWith("http://") ||
|
|
276
|
+
href.startsWith("https://") ||
|
|
277
|
+
href.startsWith("mailto:")
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function parseReactions(
|
|
282
|
+
commentEl: ReturnType<typeof parseBwXml>,
|
|
283
|
+
): CommentReaction[] {
|
|
284
|
+
const container = firstChild(commentEl, "reactions");
|
|
285
|
+
if (!container) return [];
|
|
286
|
+
const out: CommentReaction[] = [];
|
|
287
|
+
for (const el of childrenOf(container, "reaction")) {
|
|
288
|
+
const emoji = el.attributes["emoji"];
|
|
289
|
+
const authorId = el.attributes["authorId"];
|
|
290
|
+
const reactedAt = el.attributes["reactedAt"];
|
|
291
|
+
if (!emoji || !authorId || !reactedAt) continue;
|
|
292
|
+
out.push({ emoji, authorId, reactedAt });
|
|
293
|
+
}
|
|
294
|
+
return out;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function parseLabels(
|
|
298
|
+
commentEl: ReturnType<typeof parseBwXml>,
|
|
299
|
+
): CommentLabel[] {
|
|
300
|
+
const container = firstChild(commentEl, "labels");
|
|
301
|
+
if (!container) return [];
|
|
302
|
+
const out: CommentLabel[] = [];
|
|
303
|
+
for (const el of childrenOf(container, "label")) {
|
|
304
|
+
const key = el.attributes["key"];
|
|
305
|
+
if (!key) continue;
|
|
306
|
+
const label: CommentLabel = { key, text: textOf(el) };
|
|
307
|
+
if (el.attributes["color"] !== undefined) label.color = el.attributes["color"];
|
|
308
|
+
out.push(label);
|
|
309
|
+
}
|
|
310
|
+
return out;
|
|
311
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import type { ExternalCustody } from "../../api/external-custody-types.ts";
|
|
2
|
+
import {
|
|
3
|
+
attrNumber,
|
|
4
|
+
childrenOf,
|
|
5
|
+
parseBwXml,
|
|
6
|
+
renderElement,
|
|
7
|
+
stripNs,
|
|
8
|
+
} from "./bw-xml.ts";
|
|
9
|
+
|
|
10
|
+
const NS_URI = "urn:beyondwork:workflow-payload:1";
|
|
11
|
+
|
|
12
|
+
export function buildExternalCustodyXml(custody: ExternalCustody): string {
|
|
13
|
+
const stripped = renderElement("bw:strippedComments", {}, [
|
|
14
|
+
custody.strippedCommentIds
|
|
15
|
+
.map((id) => renderElement("bw:commentRef", { commentId: id }))
|
|
16
|
+
.join(""),
|
|
17
|
+
]);
|
|
18
|
+
const participants = custody.strippedParticipantIds.length
|
|
19
|
+
? renderElement("bw:strippedParticipants", {}, [
|
|
20
|
+
custody.strippedParticipantIds
|
|
21
|
+
.map((id) => renderElement("bw:userRef", { id }))
|
|
22
|
+
.join(""),
|
|
23
|
+
])
|
|
24
|
+
: "";
|
|
25
|
+
return renderElement(
|
|
26
|
+
"bw:externalCustody",
|
|
27
|
+
{
|
|
28
|
+
"xmlns:bw": NS_URI,
|
|
29
|
+
schemaVersion: String(custody.schemaVersion),
|
|
30
|
+
custodyId: custody.custodyId,
|
|
31
|
+
originDocumentId: custody.originDocumentId,
|
|
32
|
+
originPayloadId: custody.originPayloadId,
|
|
33
|
+
originContentHash: custody.originContentHash,
|
|
34
|
+
sentAt: custody.sentAt,
|
|
35
|
+
sentBy: custody.sentBy,
|
|
36
|
+
recipient: custody.recipient,
|
|
37
|
+
archiveRef: custody.archiveRef,
|
|
38
|
+
},
|
|
39
|
+
[stripped, participants],
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function parseExternalCustodyXml(xml: string): ExternalCustody {
|
|
44
|
+
const root = parseBwXml(xml);
|
|
45
|
+
if (stripNs(root.name) !== "externalCustody") {
|
|
46
|
+
throw new Error(
|
|
47
|
+
`parseExternalCustodyXml: expected <bw:externalCustody>, got <${root.name}>`,
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
const schemaVersion = attrNumber(root.attributes["schemaVersion"]) ?? 1;
|
|
51
|
+
if (schemaVersion !== 1) {
|
|
52
|
+
throw new Error(
|
|
53
|
+
`parseExternalCustodyXml: unsupported schemaVersion=${schemaVersion}`,
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const required = [
|
|
58
|
+
"custodyId",
|
|
59
|
+
"originDocumentId",
|
|
60
|
+
"originPayloadId",
|
|
61
|
+
"originContentHash",
|
|
62
|
+
"sentAt",
|
|
63
|
+
"sentBy",
|
|
64
|
+
"recipient",
|
|
65
|
+
"archiveRef",
|
|
66
|
+
] as const;
|
|
67
|
+
for (const key of required) {
|
|
68
|
+
if (!root.attributes[key]) {
|
|
69
|
+
throw new Error(
|
|
70
|
+
`parseExternalCustodyXml: missing required attribute ${key}`,
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const strippedContainer = childrenOf(root, "strippedComments")[0];
|
|
76
|
+
const strippedCommentIds = strippedContainer
|
|
77
|
+
? childrenOf(strippedContainer, "commentRef")
|
|
78
|
+
.map((el) => el.attributes["commentId"])
|
|
79
|
+
.filter((id): id is string => Boolean(id))
|
|
80
|
+
: [];
|
|
81
|
+
|
|
82
|
+
const participantsContainer = childrenOf(root, "strippedParticipants")[0];
|
|
83
|
+
const strippedParticipantIds = participantsContainer
|
|
84
|
+
? childrenOf(participantsContainer, "userRef")
|
|
85
|
+
.map((el) => el.attributes["id"])
|
|
86
|
+
.filter((id): id is string => Boolean(id))
|
|
87
|
+
: [];
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
schemaVersion: 1,
|
|
91
|
+
custodyId: root.attributes["custodyId"]!,
|
|
92
|
+
originDocumentId: root.attributes["originDocumentId"]!,
|
|
93
|
+
originPayloadId: root.attributes["originPayloadId"]!,
|
|
94
|
+
originContentHash: root.attributes["originContentHash"]!,
|
|
95
|
+
sentAt: root.attributes["sentAt"]!,
|
|
96
|
+
sentBy: root.attributes["sentBy"]!,
|
|
97
|
+
recipient: root.attributes["recipient"]!,
|
|
98
|
+
archiveRef: root.attributes["archiveRef"]!,
|
|
99
|
+
strippedCommentIds,
|
|
100
|
+
strippedParticipantIds,
|
|
101
|
+
};
|
|
102
|
+
}
|