@beyondwork/docx-react-component 1.0.83 → 1.0.85
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 +1 -1
- package/src/api/internal/build-ref-projections.ts +3 -0
- package/src/api/public-types.ts +86 -4
- package/src/api/v3/_runtime-handle.ts +15 -0
- package/src/api/v3/runtime/content.ts +148 -1
- package/src/api/v3/runtime/formatting.ts +41 -0
- package/src/api/v3/runtime/review.ts +98 -0
- package/src/api/v3/runtime/workflow.ts +154 -6
- package/src/core/commands/index.ts +81 -25
- package/src/core/state/editor-state.ts +15 -0
- package/src/io/export/serialize-main-document.ts +72 -6
- package/src/io/ooxml/header-footer-reference.ts +38 -0
- package/src/io/ooxml/parse-headers-footers.ts +11 -23
- package/src/io/ooxml/parse-main-document.ts +7 -10
- package/src/io/ooxml/workflow-payload-validator.ts +24 -0
- package/src/io/ooxml/workflow-payload.ts +12 -0
- package/src/model/canonical-document.ts +9 -0
- package/src/model/review/comment-types.ts +2 -0
- package/src/runtime/document-runtime.ts +718 -68
- package/src/runtime/formatting/field/resolver.ts +73 -8
- package/src/runtime/layout/layout-engine-version.ts +31 -12
- package/src/runtime/layout/paginated-layout-engine.ts +18 -11
- package/src/runtime/layout/public-facet.ts +119 -16
- package/src/runtime/layout/resolve-page-fields.ts +68 -6
- package/src/runtime/layout/resolve-page-previews.ts +1 -1
- package/src/runtime/scopes/_scope-dependencies.ts +1 -0
- package/src/runtime/scopes/action-validation.ts +54 -45
- package/src/runtime/scopes/workflow-overlap.ts +41 -9
- package/src/runtime/suggestions-snapshot.ts +24 -0
- package/src/runtime/surface-projection.ts +59 -2
- package/src/runtime/workflow/coordinator.ts +66 -14
- package/src/runtime/workflow/scope-writer.ts +83 -5
- package/src/shell/ref-commands.ts +3 -354
- package/src/shell/session-bootstrap.ts +10 -0
- package/src/ui/WordReviewEditor.tsx +99 -9
- package/src/ui/editor-command-bag.ts +3 -1
- package/src/ui/headless/revision-decoration-model.ts +13 -0
- package/src/ui/headless/selection-tool-types.ts +2 -0
- package/src/ui-tailwind/chrome/tw-suggestion-card.tsx +7 -3
- package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +175 -25
- package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +1 -1
- package/src/ui-tailwind/editor-surface/pm-decorations.ts +12 -0
- package/src/ui-tailwind/editor-surface/pm-page-break-decorations.ts +18 -30
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +1 -1
- package/src/ui-tailwind/editor-surface/tw-page-block-view.tsx +1 -1
- package/src/ui-tailwind/page-stack/tw-page-chrome-entry.tsx +20 -11
- package/src/ui-tailwind/page-stack/tw-page-footer-band.tsx +9 -4
- package/src/ui-tailwind/page-stack/tw-page-header-band.tsx +12 -7
- package/src/ui-tailwind/page-stack/tw-page-stack-chrome-layer.tsx +29 -10
- package/src/ui-tailwind/page-stack/tw-region-block-renderer.tsx +1 -1
- package/src/ui-tailwind/review-workspace/types.ts +3 -2
- package/src/ui-tailwind/review-workspace/use-page-markers.ts +11 -1
- package/src/ui-tailwind/toolbar/tw-role-action-region.tsx +2 -1
- package/src/ui-tailwind/toolbar/tw-toolbar.tsx +2 -1
- package/src/ui-tailwind/tw-review-workspace.tsx +18 -2
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* @endStateApi v3 — `runtime.workflow` family.
|
|
3
3
|
*
|
|
4
4
|
* queryScopes (live) / getMarkup (live) / getGuard (live) /
|
|
5
|
-
* createScope (live-with-adapter) /
|
|
5
|
+
* createScope (live-with-adapter) / setScopeGuardPolicy (live) /
|
|
6
|
+
* attachMetadata (live-with-adapter) /
|
|
6
7
|
* getVisibilityPolicy · getVisibilityPolicies · setVisibilityPolicy ·
|
|
7
8
|
* clearVisibilityPolicy (live — W10 state-classes X1) /
|
|
8
9
|
* scopeTags (live — coord-10 L11-4 tag-catalog read).
|
|
@@ -14,6 +15,8 @@ import type {
|
|
|
14
15
|
EditorStoryTarget,
|
|
15
16
|
OverlayKind,
|
|
16
17
|
OverlayVisibilityPolicy,
|
|
18
|
+
ScopeVisibility,
|
|
19
|
+
WorkflowScopeGuardPolicy,
|
|
17
20
|
WorkflowMarkupModePolicy,
|
|
18
21
|
} from "../../public-types.ts";
|
|
19
22
|
import { emitUxResponse } from "../_ux-response.ts";
|
|
@@ -82,6 +85,13 @@ export interface CreateScopeInput {
|
|
|
82
85
|
readonly blockId: string;
|
|
83
86
|
readonly mode?: "edit" | "suggest" | "comment" | "view";
|
|
84
87
|
readonly label?: string;
|
|
88
|
+
/** Per-scope visibility. Controls chrome/query presentation only. */
|
|
89
|
+
readonly visibility?: ScopeVisibility;
|
|
90
|
+
/**
|
|
91
|
+
* Explicit edit-enforcement posture. Use `"insert-only"` for
|
|
92
|
+
* template-slot allowlists; use `"read-only"` to lock the scope range.
|
|
93
|
+
*/
|
|
94
|
+
readonly guardPolicy?: WorkflowScopeGuardPolicy;
|
|
85
95
|
/**
|
|
86
96
|
* Coord-06 §13d — per-scope edge stickiness. Defaults to
|
|
87
97
|
* `{ start: 1, end: -1 }` (greedy — absorbs boundary inserts, the
|
|
@@ -112,10 +122,24 @@ export interface CreateScopeInput {
|
|
|
112
122
|
| "runtime-handle";
|
|
113
123
|
}
|
|
114
124
|
|
|
115
|
-
export
|
|
116
|
-
readonly scopeId: string;
|
|
117
|
-
readonly
|
|
118
|
-
|
|
125
|
+
export type CreateScopeResult =
|
|
126
|
+
| { readonly scopeId: string; readonly status: "created" }
|
|
127
|
+
| { readonly scopeId: ""; readonly status: "block-not-found" }
|
|
128
|
+
| {
|
|
129
|
+
readonly scopeId: "";
|
|
130
|
+
readonly status: "range-invalid";
|
|
131
|
+
readonly reason:
|
|
132
|
+
| "range-exceeds-story-length"
|
|
133
|
+
| "non-paragraph-target"
|
|
134
|
+
| "empty-document";
|
|
135
|
+
readonly from: number;
|
|
136
|
+
readonly to: number;
|
|
137
|
+
readonly storyLength: number;
|
|
138
|
+
readonly blockIndex?: number;
|
|
139
|
+
readonly blockKind?: string;
|
|
140
|
+
readonly message: string;
|
|
141
|
+
readonly nextStep: string;
|
|
142
|
+
};
|
|
119
143
|
|
|
120
144
|
/**
|
|
121
145
|
* Input for `createScopeFromAnchor` — sub-block marker-backed scope.
|
|
@@ -144,6 +168,8 @@ export interface CreateScopeFromAnchorInput {
|
|
|
144
168
|
};
|
|
145
169
|
readonly mode?: "edit" | "suggest" | "comment" | "view";
|
|
146
170
|
readonly label?: string;
|
|
171
|
+
readonly visibility?: ScopeVisibility;
|
|
172
|
+
readonly guardPolicy?: WorkflowScopeGuardPolicy;
|
|
147
173
|
readonly assoc?: { readonly start: -1 | 1; readonly end: -1 | 1 };
|
|
148
174
|
readonly stableRefHint?:
|
|
149
175
|
| "scope-id"
|
|
@@ -174,6 +200,22 @@ export type CreateScopeFromAnchorResult =
|
|
|
174
200
|
readonly nextStep: string;
|
|
175
201
|
};
|
|
176
202
|
|
|
203
|
+
export interface SetScopeGuardPolicyInput {
|
|
204
|
+
readonly scopeId: string;
|
|
205
|
+
readonly guardPolicy: WorkflowScopeGuardPolicy;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export type SetScopeGuardPolicyResult =
|
|
209
|
+
| {
|
|
210
|
+
readonly status: "updated";
|
|
211
|
+
readonly scopeId: string;
|
|
212
|
+
readonly guardPolicy: WorkflowScopeGuardPolicy;
|
|
213
|
+
}
|
|
214
|
+
| {
|
|
215
|
+
readonly status: "scope-not-found";
|
|
216
|
+
readonly scopeId: string;
|
|
217
|
+
};
|
|
218
|
+
|
|
177
219
|
export const createScopeMetadata: ApiV3FnMetadata = {
|
|
178
220
|
name: "runtime.workflow.createScope",
|
|
179
221
|
status: "live-with-adapter",
|
|
@@ -216,6 +258,51 @@ export const createScopeFromAnchorMetadata: ApiV3FnMetadata = {
|
|
|
216
258
|
"§Runtime API § runtime.workflow.createScopeFromAnchor. Companion to createScope({blockId}) for sub-block ranges. Consumes from/to once to plant inline scope_marker_start/end; the returned scopeId is the durable reference (KI-P9).",
|
|
217
259
|
};
|
|
218
260
|
|
|
261
|
+
export const getScopeGuardPolicyMetadata: ApiV3FnMetadata = {
|
|
262
|
+
name: "runtime.workflow.getScopeGuardPolicy",
|
|
263
|
+
status: "live",
|
|
264
|
+
sourceLayer: "workflow-review",
|
|
265
|
+
liveEvidence: {
|
|
266
|
+
runnerTest: "test/api/v3/workflow-create-scope-live.test.ts",
|
|
267
|
+
commit: "guard-policy-setter",
|
|
268
|
+
},
|
|
269
|
+
uxIntent: { uiVisible: false, expectsUxResponse: "none" },
|
|
270
|
+
agentMetadata: {
|
|
271
|
+
readOrMutate: "read",
|
|
272
|
+
boundedScope: "scope",
|
|
273
|
+
auditCategory: "scope-policy",
|
|
274
|
+
},
|
|
275
|
+
stateClass: "A-canonical",
|
|
276
|
+
persistsTo: "customXml",
|
|
277
|
+
broadcastsVia: "crdt",
|
|
278
|
+
rwdReference: "§Runtime API § runtime.workflow.getScopeGuardPolicy",
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
export const setScopeGuardPolicyMetadata: ApiV3FnMetadata = {
|
|
282
|
+
name: "runtime.workflow.setScopeGuardPolicy",
|
|
283
|
+
status: "live",
|
|
284
|
+
sourceLayer: "workflow-review",
|
|
285
|
+
liveEvidence: {
|
|
286
|
+
runnerTest: "test/api/v3/workflow-create-scope-live.test.ts",
|
|
287
|
+
commit: "guard-policy-setter",
|
|
288
|
+
},
|
|
289
|
+
uxIntent: {
|
|
290
|
+
uiVisible: true,
|
|
291
|
+
expectsUxResponse: "inline-change",
|
|
292
|
+
expectedDelta:
|
|
293
|
+
"scope edit-enforcement policy updates and interaction guard recalculates",
|
|
294
|
+
},
|
|
295
|
+
agentMetadata: {
|
|
296
|
+
readOrMutate: "mutate",
|
|
297
|
+
boundedScope: "scope",
|
|
298
|
+
auditCategory: "scope-policy",
|
|
299
|
+
},
|
|
300
|
+
stateClass: "A-canonical",
|
|
301
|
+
persistsTo: "customXml",
|
|
302
|
+
broadcastsVia: "crdt",
|
|
303
|
+
rwdReference: "§Runtime API § runtime.workflow.setScopeGuardPolicy",
|
|
304
|
+
};
|
|
305
|
+
|
|
219
306
|
export interface AttachMetadataInput {
|
|
220
307
|
readonly scopeId: string;
|
|
221
308
|
readonly metadataId: string;
|
|
@@ -476,6 +563,8 @@ export function createWorkflowFamily(runtime: RuntimeApiHandle) {
|
|
|
476
563
|
blockId: input.blockId,
|
|
477
564
|
mode: input.mode,
|
|
478
565
|
label: input.label,
|
|
566
|
+
...(input.visibility ? { visibility: input.visibility } : {}),
|
|
567
|
+
...(input.guardPolicy ? { guardPolicy: input.guardPolicy } : {}),
|
|
479
568
|
...(input.assoc ? { assoc: input.assoc } : {}),
|
|
480
569
|
...(input.stableRefHint
|
|
481
570
|
? { stableRefHint: input.stableRefHint }
|
|
@@ -493,11 +582,68 @@ export function createWorkflowFamily(runtime: RuntimeApiHandle) {
|
|
|
493
582
|
: undefined,
|
|
494
583
|
});
|
|
495
584
|
if (adapterResult.status !== "created") {
|
|
496
|
-
|
|
585
|
+
if (adapterResult.status === "block-not-found") {
|
|
586
|
+
return { scopeId: "", status: "block-not-found" };
|
|
587
|
+
}
|
|
588
|
+
return {
|
|
589
|
+
scopeId: "",
|
|
590
|
+
status: "range-invalid",
|
|
591
|
+
reason: adapterResult.reason,
|
|
592
|
+
from: adapterResult.from,
|
|
593
|
+
to: adapterResult.to,
|
|
594
|
+
storyLength: adapterResult.storyLength,
|
|
595
|
+
...(adapterResult.blockIndex !== undefined
|
|
596
|
+
? { blockIndex: adapterResult.blockIndex }
|
|
597
|
+
: {}),
|
|
598
|
+
...(adapterResult.blockKind !== undefined
|
|
599
|
+
? { blockKind: adapterResult.blockKind }
|
|
600
|
+
: {}),
|
|
601
|
+
message: adapterResult.message,
|
|
602
|
+
nextStep: adapterResult.nextStep,
|
|
603
|
+
};
|
|
497
604
|
}
|
|
498
605
|
return { scopeId: adapterResult.scopeId, status: "created" };
|
|
499
606
|
},
|
|
500
607
|
|
|
608
|
+
getScopeGuardPolicy(scopeId: string): WorkflowScopeGuardPolicy {
|
|
609
|
+
// @endStateApi — live.
|
|
610
|
+
return runtime.getScopeGuardPolicy(scopeId);
|
|
611
|
+
},
|
|
612
|
+
|
|
613
|
+
setScopeGuardPolicy(
|
|
614
|
+
input: SetScopeGuardPolicyInput,
|
|
615
|
+
): SetScopeGuardPolicyResult {
|
|
616
|
+
// @endStateApi — live. Mutates the scope's explicit guard axis in the
|
|
617
|
+
// workflow overlay. Visibility stays presentation-only.
|
|
618
|
+
const overlay = runtime.getWorkflowOverlay();
|
|
619
|
+
const existingScope = overlay?.scopes.find(
|
|
620
|
+
(scope) => scope.scopeId === input.scopeId,
|
|
621
|
+
);
|
|
622
|
+
if (!existingScope) {
|
|
623
|
+
return { status: "scope-not-found", scopeId: input.scopeId };
|
|
624
|
+
}
|
|
625
|
+
runtime.setScopeGuardPolicy(input.scopeId, input.guardPolicy);
|
|
626
|
+
emitUxResponse(runtime, {
|
|
627
|
+
apiFn: setScopeGuardPolicyMetadata.name,
|
|
628
|
+
intent: setScopeGuardPolicyMetadata.uxIntent.expectedDelta ?? "",
|
|
629
|
+
mockOrLive: "live",
|
|
630
|
+
uiVisible: true,
|
|
631
|
+
expectedDelta: setScopeGuardPolicyMetadata.uxIntent.expectedDelta,
|
|
632
|
+
actualDelta: {
|
|
633
|
+
kind: "inline-change",
|
|
634
|
+
payload: {
|
|
635
|
+
scopeId: input.scopeId,
|
|
636
|
+
guardPolicy: input.guardPolicy,
|
|
637
|
+
},
|
|
638
|
+
},
|
|
639
|
+
});
|
|
640
|
+
return {
|
|
641
|
+
status: "updated",
|
|
642
|
+
scopeId: input.scopeId,
|
|
643
|
+
guardPolicy: input.guardPolicy,
|
|
644
|
+
};
|
|
645
|
+
},
|
|
646
|
+
|
|
501
647
|
createScopeFromAnchor(
|
|
502
648
|
input: CreateScopeFromAnchorInput,
|
|
503
649
|
): CreateScopeFromAnchorResult {
|
|
@@ -510,6 +656,8 @@ export function createWorkflowFamily(runtime: RuntimeApiHandle) {
|
|
|
510
656
|
anchor: input.anchor,
|
|
511
657
|
mode: input.mode,
|
|
512
658
|
label: input.label,
|
|
659
|
+
...(input.visibility ? { visibility: input.visibility } : {}),
|
|
660
|
+
...(input.guardPolicy ? { guardPolicy: input.guardPolicy } : {}),
|
|
513
661
|
...(input.assoc ? { assoc: input.assoc } : {}),
|
|
514
662
|
...(input.stableRefHint
|
|
515
663
|
? { stableRefHint: input.stableRefHint }
|
|
@@ -58,7 +58,9 @@ import {
|
|
|
58
58
|
} from "./text-commands.ts";
|
|
59
59
|
import type {
|
|
60
60
|
BlockNode,
|
|
61
|
+
InlineNode,
|
|
61
62
|
MutableCanonicalDocument,
|
|
63
|
+
ParagraphNode,
|
|
62
64
|
RevisionRecord as CanonicalRevisionRecord,
|
|
63
65
|
} from "../../model/canonical-document.ts";
|
|
64
66
|
import { remapCommentThreads } from "../../review/store/comment-remapping.ts";
|
|
@@ -2314,39 +2316,93 @@ function isSingleParagraphSuggestingRange(
|
|
|
2314
2316
|
to: number,
|
|
2315
2317
|
): boolean {
|
|
2316
2318
|
const ranges: Array<{ start: number; end: number }> = [];
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
+
collectSuggestingParagraphRanges(
|
|
2320
|
+
document.content.children,
|
|
2321
|
+
0,
|
|
2322
|
+
ranges,
|
|
2323
|
+
true,
|
|
2324
|
+
);
|
|
2325
|
+
|
|
2326
|
+
return ranges.some((range) => from >= range.start && to <= range.end);
|
|
2327
|
+
}
|
|
2328
|
+
|
|
2329
|
+
function collectSuggestingParagraphRanges(
|
|
2330
|
+
blocks: readonly BlockNode[],
|
|
2331
|
+
startCursor: number,
|
|
2332
|
+
output: Array<{ start: number; end: number }>,
|
|
2333
|
+
addRootParagraphBoundaries: boolean,
|
|
2334
|
+
): number {
|
|
2335
|
+
let cursor = startCursor;
|
|
2336
|
+
|
|
2337
|
+
for (let index = 0; index < blocks.length; index += 1) {
|
|
2338
|
+
const block = blocks[index];
|
|
2339
|
+
if (!block) {
|
|
2340
|
+
continue;
|
|
2341
|
+
}
|
|
2319
2342
|
|
|
2320
|
-
for (const block of document.content.children) {
|
|
2321
2343
|
if (block.type === "paragraph") {
|
|
2322
|
-
if (previousWasParagraph) {
|
|
2323
|
-
cursor += 1;
|
|
2324
|
-
}
|
|
2325
2344
|
const start = cursor;
|
|
2326
|
-
cursor += block
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2345
|
+
cursor += paragraphLogicalLength(block);
|
|
2346
|
+
output.push({ start, end: cursor });
|
|
2347
|
+
} else if (block.type === "table") {
|
|
2348
|
+
for (const row of block.rows) {
|
|
2349
|
+
for (const cell of row.cells) {
|
|
2350
|
+
cursor = collectSuggestingParagraphRanges(
|
|
2351
|
+
cell.children,
|
|
2352
|
+
cursor,
|
|
2353
|
+
output,
|
|
2354
|
+
false,
|
|
2355
|
+
);
|
|
2337
2356
|
}
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2357
|
+
}
|
|
2358
|
+
} else if (block.type === "sdt") {
|
|
2359
|
+
cursor = collectSuggestingParagraphRanges(
|
|
2360
|
+
block.children,
|
|
2361
|
+
cursor,
|
|
2362
|
+
output,
|
|
2363
|
+
false,
|
|
2364
|
+
);
|
|
2365
|
+
} else {
|
|
2366
|
+
cursor += 1;
|
|
2343
2367
|
}
|
|
2344
2368
|
|
|
2345
|
-
|
|
2346
|
-
|
|
2369
|
+
if (
|
|
2370
|
+
addRootParagraphBoundaries &&
|
|
2371
|
+
index < blocks.length - 1 &&
|
|
2372
|
+
blocks[index + 1]?.type === "paragraph"
|
|
2373
|
+
) {
|
|
2374
|
+
cursor += 1;
|
|
2375
|
+
}
|
|
2347
2376
|
}
|
|
2348
2377
|
|
|
2349
|
-
return
|
|
2378
|
+
return cursor;
|
|
2379
|
+
}
|
|
2380
|
+
|
|
2381
|
+
function paragraphLogicalLength(paragraph: ParagraphNode): number {
|
|
2382
|
+
return paragraph.children.reduce<number>(
|
|
2383
|
+
(size, child) => size + inlineLogicalLength(child),
|
|
2384
|
+
0,
|
|
2385
|
+
);
|
|
2386
|
+
}
|
|
2387
|
+
|
|
2388
|
+
function inlineLogicalLength(node: InlineNode): number {
|
|
2389
|
+
switch (node.type) {
|
|
2390
|
+
case "text":
|
|
2391
|
+
return node.text.length;
|
|
2392
|
+
case "hyperlink":
|
|
2393
|
+
case "field":
|
|
2394
|
+
return node.children.reduce<number>(
|
|
2395
|
+
(size, child) => size + inlineLogicalLength(child as InlineNode),
|
|
2396
|
+
0,
|
|
2397
|
+
);
|
|
2398
|
+
case "bookmark_start":
|
|
2399
|
+
case "bookmark_end":
|
|
2400
|
+
case "scope_marker_start":
|
|
2401
|
+
case "scope_marker_end":
|
|
2402
|
+
return 0;
|
|
2403
|
+
default:
|
|
2404
|
+
return 1;
|
|
2405
|
+
}
|
|
2350
2406
|
}
|
|
2351
2407
|
|
|
2352
2408
|
function applySuggestingInsert(
|
|
@@ -839,6 +839,21 @@ export function normalizeCommentThreadRecord(value: unknown): CommentThreadRecor
|
|
|
839
839
|
typeof record.metadata.rootParaId === "string"
|
|
840
840
|
? record.metadata.rootParaId
|
|
841
841
|
: undefined,
|
|
842
|
+
linkedRevisionId:
|
|
843
|
+
typeof record.metadata.linkedRevisionId === "string"
|
|
844
|
+
? record.metadata.linkedRevisionId
|
|
845
|
+
: undefined,
|
|
846
|
+
detachedReason:
|
|
847
|
+
record.metadata.detachedReason === "incomplete-markers" ||
|
|
848
|
+
record.metadata.detachedReason === "multi-paragraph" ||
|
|
849
|
+
record.metadata.detachedReason === "opaque-region" ||
|
|
850
|
+
record.metadata.detachedReason === "revision-overlap"
|
|
851
|
+
? record.metadata.detachedReason
|
|
852
|
+
: undefined,
|
|
853
|
+
actionabilityNote:
|
|
854
|
+
typeof record.metadata.actionabilityNote === "string"
|
|
855
|
+
? record.metadata.actionabilityNote
|
|
856
|
+
: undefined,
|
|
842
857
|
}
|
|
843
858
|
: undefined,
|
|
844
859
|
};
|
|
@@ -89,6 +89,14 @@ interface SerializationState {
|
|
|
89
89
|
usedTextIds: Set<string>;
|
|
90
90
|
/** Deterministic PRNG-less counter used when we mint fresh ids. */
|
|
91
91
|
mintedParaIdCounter: number;
|
|
92
|
+
/**
|
|
93
|
+
* Scope markers export as OOXML bookmarks. `w:bookmarkStart/@w:id` is a
|
|
94
|
+
* decimal number in WordprocessingML, so scope ids cannot be written into
|
|
95
|
+
* it directly.
|
|
96
|
+
*/
|
|
97
|
+
usedBookmarkIds: Set<string>;
|
|
98
|
+
scopeBookmarkIds: Map<string, string>;
|
|
99
|
+
nextScopeBookmarkId: number;
|
|
92
100
|
}
|
|
93
101
|
|
|
94
102
|
interface InlineSerializationResult {
|
|
@@ -117,6 +125,61 @@ function serializePerformanceNow(): number {
|
|
|
117
125
|
return Date.now();
|
|
118
126
|
}
|
|
119
127
|
|
|
128
|
+
function collectNumericBookmarkIds(content: DocumentRootNode): Set<string> {
|
|
129
|
+
const used = new Set<string>();
|
|
130
|
+
const visit = (node: unknown): void => {
|
|
131
|
+
if (!node || typeof node !== "object") return;
|
|
132
|
+
const typed = node as {
|
|
133
|
+
type?: string;
|
|
134
|
+
bookmarkId?: unknown;
|
|
135
|
+
children?: unknown;
|
|
136
|
+
rows?: unknown;
|
|
137
|
+
cells?: unknown;
|
|
138
|
+
};
|
|
139
|
+
if (
|
|
140
|
+
(typed.type === "bookmark_start" || typed.type === "bookmark_end") &&
|
|
141
|
+
typeof typed.bookmarkId === "string"
|
|
142
|
+
) {
|
|
143
|
+
const normalized = normalizeDecimalBookmarkId(typed.bookmarkId);
|
|
144
|
+
if (normalized !== null) used.add(normalized);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (Array.isArray(typed.children)) {
|
|
148
|
+
for (const child of typed.children) visit(child);
|
|
149
|
+
}
|
|
150
|
+
if (Array.isArray(typed.rows)) {
|
|
151
|
+
for (const row of typed.rows) visit(row);
|
|
152
|
+
}
|
|
153
|
+
if (Array.isArray(typed.cells)) {
|
|
154
|
+
for (const cell of typed.cells) visit(cell);
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
visit(content);
|
|
159
|
+
return used;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function normalizeDecimalBookmarkId(id: string): string | null {
|
|
163
|
+
if (!/^\d+$/u.test(id)) return null;
|
|
164
|
+
const stripped = id.replace(/^0+/u, "");
|
|
165
|
+
return stripped.length > 0 ? stripped : "0";
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function scopeBookmarkIdFor(state: SerializationState, scopeId: string): string {
|
|
169
|
+
const existing = state.scopeBookmarkIds.get(scopeId);
|
|
170
|
+
if (existing !== undefined) return existing;
|
|
171
|
+
|
|
172
|
+
let next = state.nextScopeBookmarkId;
|
|
173
|
+
while (state.usedBookmarkIds.has(String(next))) {
|
|
174
|
+
next += 1;
|
|
175
|
+
}
|
|
176
|
+
const bookmarkId = String(next);
|
|
177
|
+
state.nextScopeBookmarkId = next + 1;
|
|
178
|
+
state.usedBookmarkIds.add(bookmarkId);
|
|
179
|
+
state.scopeBookmarkIds.set(scopeId, bookmarkId);
|
|
180
|
+
return bookmarkId;
|
|
181
|
+
}
|
|
182
|
+
|
|
120
183
|
export function serializeMainDocument(
|
|
121
184
|
content: DocumentRootNode,
|
|
122
185
|
preservation: PreservationStore = { opaqueFragments: {}, packageParts: {} },
|
|
@@ -150,6 +213,9 @@ export function serializeMainDocument(
|
|
|
150
213
|
usedParaIds: new Set<string>(),
|
|
151
214
|
usedTextIds: new Set<string>(),
|
|
152
215
|
mintedParaIdCounter: 0,
|
|
216
|
+
usedBookmarkIds: collectNumericBookmarkIds(content),
|
|
217
|
+
scopeBookmarkIds: new Map<string, string>(),
|
|
218
|
+
nextScopeBookmarkId: 100000,
|
|
153
219
|
};
|
|
154
220
|
const suffix = `</w:body>\n</w:document>`;
|
|
155
221
|
const bodyPieces: string[] = [];
|
|
@@ -679,9 +745,9 @@ function serializeTableInlineNode(
|
|
|
679
745
|
return `<w:bookmarkEnd w:id="${escapeXmlAttribute(node.bookmarkId)}"/>`;
|
|
680
746
|
case "scope_marker_start": {
|
|
681
747
|
// S1 — scope markers export as w:bookmarkStart with the reserved
|
|
682
|
-
// `bw:scope:` name prefix. The synthetic w:id is
|
|
683
|
-
//
|
|
684
|
-
const bkId =
|
|
748
|
+
// `bw:scope:` name prefix. The synthetic w:id is numeric because
|
|
749
|
+
// WordprocessingML bookmark ids are ST_DecimalNumber values.
|
|
750
|
+
const bkId = scopeBookmarkIdFor(state, node.scopeId);
|
|
685
751
|
const name = `${SCOPE_MARKER_BOOKMARK_PREFIX}${node.scopeId}`;
|
|
686
752
|
return (
|
|
687
753
|
`<w:bookmarkStart w:id="${escapeXmlAttribute(bkId)}"` +
|
|
@@ -689,7 +755,7 @@ function serializeTableInlineNode(
|
|
|
689
755
|
);
|
|
690
756
|
}
|
|
691
757
|
case "scope_marker_end": {
|
|
692
|
-
const bkId =
|
|
758
|
+
const bkId = scopeBookmarkIdFor(state, node.scopeId);
|
|
693
759
|
return `<w:bookmarkEnd w:id="${escapeXmlAttribute(bkId)}"/>`;
|
|
694
760
|
}
|
|
695
761
|
case "footnote_ref": {
|
|
@@ -1258,7 +1324,7 @@ function serializeInlineNode(
|
|
|
1258
1324
|
case "scope_marker_start": {
|
|
1259
1325
|
// S1 — mirror the bookmark_start shape with the reserved `bw:scope:`
|
|
1260
1326
|
// name prefix. See serializeInline() above for the same convention.
|
|
1261
|
-
const bkId =
|
|
1327
|
+
const bkId = scopeBookmarkIdFor(state, node.scopeId);
|
|
1262
1328
|
const name = `${SCOPE_MARKER_BOOKMARK_PREFIX}${node.scopeId}`;
|
|
1263
1329
|
const xml =
|
|
1264
1330
|
`<w:bookmarkStart w:id="${escapeXmlAttribute(bkId)}"` +
|
|
@@ -1269,7 +1335,7 @@ function serializeInlineNode(
|
|
|
1269
1335
|
return { xml, cursor, boundaries };
|
|
1270
1336
|
}
|
|
1271
1337
|
case "scope_marker_end": {
|
|
1272
|
-
const bkId =
|
|
1338
|
+
const bkId = scopeBookmarkIdFor(state, node.scopeId);
|
|
1273
1339
|
const xml = `<w:bookmarkEnd w:id="${escapeXmlAttribute(bkId)}"/>`;
|
|
1274
1340
|
const boundaries = new Map<number, number>();
|
|
1275
1341
|
boundaries.set(cursor, xmlOffset);
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { HeaderFooterVariant } from "../../model/canonical-document.ts";
|
|
2
|
+
|
|
3
|
+
interface HeaderFooterReferenceElement {
|
|
4
|
+
attributes: Record<string, string>;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface ParsedHeaderFooterReferenceAttributes {
|
|
8
|
+
variant: HeaderFooterVariant;
|
|
9
|
+
relationshipId: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function readHeaderFooterReferenceAttributes(
|
|
13
|
+
element: HeaderFooterReferenceElement,
|
|
14
|
+
): ParsedHeaderFooterReferenceAttributes | undefined {
|
|
15
|
+
const relationshipId =
|
|
16
|
+
element.attributes["r:id"] ??
|
|
17
|
+
element.attributes["r:Id"] ??
|
|
18
|
+
element.attributes.id ??
|
|
19
|
+
element.attributes.Id;
|
|
20
|
+
if (!relationshipId) {
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
variant: toHeaderFooterVariant(element.attributes["w:type"] ?? element.attributes.type),
|
|
26
|
+
relationshipId,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function toHeaderFooterVariant(raw: string | undefined): HeaderFooterVariant {
|
|
31
|
+
if (raw === "first") {
|
|
32
|
+
return "first";
|
|
33
|
+
}
|
|
34
|
+
if (raw === "even") {
|
|
35
|
+
return "even";
|
|
36
|
+
}
|
|
37
|
+
return "default";
|
|
38
|
+
}
|
|
@@ -20,6 +20,7 @@ import { classifyFieldInstruction } from "./parse-fields.ts";
|
|
|
20
20
|
import { isSafeTableFieldInstruction } from "./table-opaque-preservation.ts";
|
|
21
21
|
import { parseXmlWithOffsets as parseXml } from "./xml-parser.ts";
|
|
22
22
|
import { localName, readStringAttr } from "./xml-attr-helpers.ts";
|
|
23
|
+
import { readHeaderFooterReferenceAttributes } from "./header-footer-reference.ts";
|
|
23
24
|
import {
|
|
24
25
|
readCellBorders,
|
|
25
26
|
readCellCnfStyle,
|
|
@@ -200,40 +201,27 @@ function extractSectPrRefs(
|
|
|
200
201
|
const name = localName(child.name);
|
|
201
202
|
if (name === "headerReference" || name === "footerReference") {
|
|
202
203
|
const kind: "header" | "footer" = name === "headerReference" ? "header" : "footer";
|
|
203
|
-
const
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
const relationshipId =
|
|
207
|
-
child.attributes["r:id"] ??
|
|
208
|
-
child.attributes["r:Id"] ??
|
|
209
|
-
child.attributes.id ??
|
|
210
|
-
child.attributes.Id ??
|
|
211
|
-
"";
|
|
212
|
-
|
|
213
|
-
if (relationshipId) {
|
|
204
|
+
const ref = readHeaderFooterReferenceAttributes(child);
|
|
205
|
+
|
|
206
|
+
if (ref) {
|
|
214
207
|
// Avoid duplicates (multiple sectPr may reference same header)
|
|
215
|
-
const dedupeKey = `${kind}:${variant}:${relationshipId}`;
|
|
208
|
+
const dedupeKey = `${kind}:${ref.variant}:${ref.relationshipId}`;
|
|
216
209
|
const alreadyAdded = refs.some(
|
|
217
210
|
(ref) => `${ref.kind}:${ref.variant}:${ref.relationshipId}` === dedupeKey,
|
|
218
211
|
);
|
|
219
212
|
if (!alreadyAdded) {
|
|
220
|
-
refs.push({
|
|
213
|
+
refs.push({
|
|
214
|
+
variant: ref.variant,
|
|
215
|
+
relationshipId: ref.relationshipId,
|
|
216
|
+
kind,
|
|
217
|
+
sectionIndex,
|
|
218
|
+
});
|
|
221
219
|
}
|
|
222
220
|
}
|
|
223
221
|
}
|
|
224
222
|
}
|
|
225
223
|
}
|
|
226
224
|
|
|
227
|
-
function toHeaderFooterVariant(raw: string): HeaderFooterVariant {
|
|
228
|
-
if (raw === "first") {
|
|
229
|
-
return "first";
|
|
230
|
-
}
|
|
231
|
-
if (raw === "even") {
|
|
232
|
-
return "even";
|
|
233
|
-
}
|
|
234
|
-
return "default";
|
|
235
|
-
}
|
|
236
|
-
|
|
237
225
|
function parseHdrFtrXml(
|
|
238
226
|
xml: string,
|
|
239
227
|
rootLocalName: "hdr" | "ftr",
|
|
@@ -21,8 +21,6 @@ import type {
|
|
|
21
21
|
PageMargins,
|
|
22
22
|
ColumnProperties,
|
|
23
23
|
PageNumbering,
|
|
24
|
-
HeaderFooterReference,
|
|
25
|
-
HeaderFooterVariant,
|
|
26
24
|
SectionDocumentGrid,
|
|
27
25
|
SectionLineNumbering,
|
|
28
26
|
SectionPageBorders,
|
|
@@ -45,6 +43,7 @@ import { parseComplexContentXml, type ChartPartLookup } from "./parse-complex-co
|
|
|
45
43
|
import { parseShapeXml, parseVmlXml } from "./parse-shapes.ts";
|
|
46
44
|
import { parseObject } from "./parse-object.ts";
|
|
47
45
|
import { parseDrawingFrame } from "./parse-drawing.ts";
|
|
46
|
+
import { readHeaderFooterReferenceAttributes } from "./header-footer-reference.ts";
|
|
48
47
|
import { readFrameProperties } from "./parse-paragraph-formatting.ts";
|
|
49
48
|
import { tableRequiresOpaquePreservation } from "./table-opaque-preservation.ts";
|
|
50
49
|
import { classifyFieldInstruction } from "./parse-fields.ts";
|
|
@@ -3756,20 +3755,18 @@ export function parseSectionPropertiesFromElement(
|
|
|
3756
3755
|
break;
|
|
3757
3756
|
}
|
|
3758
3757
|
case "headerReference": {
|
|
3759
|
-
const
|
|
3760
|
-
|
|
3761
|
-
if (variant && rId) {
|
|
3758
|
+
const ref = readHeaderFooterReferenceAttributes(child);
|
|
3759
|
+
if (ref) {
|
|
3762
3760
|
if (!props.headerReferences) props.headerReferences = [];
|
|
3763
|
-
props.headerReferences.push(
|
|
3761
|
+
props.headerReferences.push(ref);
|
|
3764
3762
|
}
|
|
3765
3763
|
break;
|
|
3766
3764
|
}
|
|
3767
3765
|
case "footerReference": {
|
|
3768
|
-
const
|
|
3769
|
-
|
|
3770
|
-
if (variant && rId) {
|
|
3766
|
+
const ref = readHeaderFooterReferenceAttributes(child);
|
|
3767
|
+
if (ref) {
|
|
3771
3768
|
if (!props.footerReferences) props.footerReferences = [];
|
|
3772
|
-
props.footerReferences.push(
|
|
3769
|
+
props.footerReferences.push(ref);
|
|
3773
3770
|
}
|
|
3774
3771
|
break;
|
|
3775
3772
|
}
|
|
@@ -40,6 +40,8 @@ export interface ValidatorIssue {
|
|
|
40
40
|
|
|
41
41
|
const OVERLAY_PERSISTENCE_VALUES = ["internal", "external"] as const;
|
|
42
42
|
const SCOPE_PERSISTENCE_VALUES = ["internal", "external", "inherit"] as const;
|
|
43
|
+
const SCOPE_VISIBILITY_VALUES = ["visible", "hidden", "invisible"] as const;
|
|
44
|
+
const SCOPE_GUARD_POLICY_VALUES = ["none", "insert-only", "read-only"] as const;
|
|
43
45
|
|
|
44
46
|
export function validateWorkflowPayloadEnvelope(xml: string): ValidatorIssue[] {
|
|
45
47
|
const issues: ValidatorIssue[] = [];
|
|
@@ -82,6 +84,28 @@ export function validateWorkflowPayloadEnvelope(xml: string): ValidatorIssue[] {
|
|
|
82
84
|
for (const scope of findAll(xml, /<bw:scope\b([^>]*?)>([\s\S]*?)<\/bw:scope>/gu)) {
|
|
83
85
|
const scopeAttrs = parseAttrs(scope.attrs);
|
|
84
86
|
const scopeId = scopeAttrs.id ?? "?";
|
|
87
|
+
if (
|
|
88
|
+
scopeAttrs.visibility !== undefined &&
|
|
89
|
+
!(SCOPE_VISIBILITY_VALUES as readonly string[]).includes(scopeAttrs.visibility)
|
|
90
|
+
) {
|
|
91
|
+
issues.push({
|
|
92
|
+
code: "unknown_enum_value",
|
|
93
|
+
path: `bw:scope[@id='${scopeId}']/@visibility`,
|
|
94
|
+
value: scopeAttrs.visibility,
|
|
95
|
+
severity: "warning",
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
if (
|
|
99
|
+
scopeAttrs.guardPolicy !== undefined &&
|
|
100
|
+
!(SCOPE_GUARD_POLICY_VALUES as readonly string[]).includes(scopeAttrs.guardPolicy)
|
|
101
|
+
) {
|
|
102
|
+
issues.push({
|
|
103
|
+
code: "unknown_enum_value",
|
|
104
|
+
path: `bw:scope[@id='${scopeId}']/@guardPolicy`,
|
|
105
|
+
value: scopeAttrs.guardPolicy,
|
|
106
|
+
severity: "warning",
|
|
107
|
+
});
|
|
108
|
+
}
|
|
85
109
|
let scopePersistence: string | undefined = scopeAttrs.metadataPersistence;
|
|
86
110
|
if (scopePersistence !== undefined) {
|
|
87
111
|
if (
|