@beyondwork/docx-react-component 1.0.41 → 1.0.42
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 +13 -1
- 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/external-custody-types.ts +74 -0
- package/src/api/participants-types.ts +18 -0
- package/src/api/public-types.ts +347 -4
- 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 +568 -1
- package/src/index.ts +118 -1
- 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/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 +271 -0
- package/src/io/ooxml/workflow-payload.ts +146 -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 +280 -93
- package/src/runtime/external-send-runtime.ts +117 -0
- package/src/runtime/layout/docx-font-loader.ts +11 -30
- package/src/runtime/layout/inert-layout-facet.ts +2 -0
- package/src/runtime/layout/layout-engine-instance.ts +122 -12
- package/src/runtime/layout/page-graph.ts +79 -7
- package/src/runtime/layout/paginated-layout-engine.ts +230 -34
- package/src/runtime/layout/public-facet.ts +185 -13
- 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/resign-payload.ts +120 -0
- package/src/runtime/tamper-gate.ts +157 -0
- package/src/runtime/workflow-markup.ts +9 -0
- package/src/runtime/workflow-rail-segments.ts +244 -5
- package/src/ui/WordReviewEditor.tsx +587 -0
- package/src/ui/editor-runtime-boundary.ts +1 -0
- package/src/ui/editor-shell-view.tsx +11 -0
- package/src/ui-tailwind/chrome/chrome-preset-model.ts +28 -0
- 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/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 +58 -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/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 +15 -13
- package/src/ui-tailwind/editor-surface/remote-cursor-plugin.ts +20 -3
- package/src/ui-tailwind/editor-surface/tw-page-block-view.tsx +15 -14
- package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +66 -0
- package/src/ui-tailwind/index.ts +32 -0
- 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 +293 -34
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
mapRevisionBoundaries,
|
|
4
4
|
type RevisionParagraphBoundary,
|
|
5
5
|
} from "../ooxml/revision-boundaries.ts";
|
|
6
|
+
import { escapeXmlAttribute } from "./escape-xml-attribute.ts";
|
|
6
7
|
|
|
7
8
|
interface XmlReplacement {
|
|
8
9
|
start: number;
|
|
@@ -360,7 +361,7 @@ function serializeRevisionAttributes(revision: RevisionRecord): string {
|
|
|
360
361
|
|
|
361
362
|
return Object.entries(attributes)
|
|
362
363
|
.filter(([, value]) => value && value.length > 0)
|
|
363
|
-
.map(([name, value]) => ` ${name}="${
|
|
364
|
+
.map(([name, value]) => ` ${name}="${escapeXmlAttribute(value)}"`)
|
|
364
365
|
.join("");
|
|
365
366
|
}
|
|
366
367
|
|
|
@@ -453,11 +454,3 @@ function applyReplacements(documentXml: string, replacements: readonly XmlReplac
|
|
|
453
454
|
|
|
454
455
|
return output;
|
|
455
456
|
}
|
|
456
|
-
|
|
457
|
-
function escapeAttribute(value: string): string {
|
|
458
|
-
return value
|
|
459
|
-
.replace(/&/g, "&")
|
|
460
|
-
.replace(/</g, "<")
|
|
461
|
-
.replace(/>/g, ">")
|
|
462
|
-
.replace(/"/g, """);
|
|
463
|
-
}
|
|
@@ -11,6 +11,7 @@ import type {
|
|
|
11
11
|
ParsedTableWidth,
|
|
12
12
|
} from "../ooxml/parse-tables.ts";
|
|
13
13
|
import { twip } from "./twip.ts";
|
|
14
|
+
import { escapeXmlAttribute } from "./escape-xml-attribute.ts";
|
|
14
15
|
|
|
15
16
|
export function serializeTable(table: ParsedTable): string {
|
|
16
17
|
const propertiesXml = table.propertiesXml ?? buildTablePropertiesXml(table);
|
|
@@ -60,10 +61,10 @@ function buildTablePropertiesXml(table: ParsedTable): string {
|
|
|
60
61
|
children.push(table.bidiVisual ? `<w:bidiVisual/>` : `<w:bidiVisual w:val="0"/>`);
|
|
61
62
|
}
|
|
62
63
|
if (table.caption !== undefined) {
|
|
63
|
-
children.push(`<w:tblCaption w:val="${
|
|
64
|
+
children.push(`<w:tblCaption w:val="${escapeXmlAttribute(table.caption)}"/>`);
|
|
64
65
|
}
|
|
65
66
|
if (table.description !== undefined) {
|
|
66
|
-
children.push(`<w:tblDescription w:val="${
|
|
67
|
+
children.push(`<w:tblDescription w:val="${escapeXmlAttribute(table.description)}"/>`);
|
|
67
68
|
}
|
|
68
69
|
if (table.floating) {
|
|
69
70
|
const floatingXml = serializeTableFloating(table.floating);
|
|
@@ -106,7 +107,7 @@ function serializeTableFloating(floating: NonNullable<ParsedTable["floating"]>):
|
|
|
106
107
|
function buildRowPropertiesXml(row: ParsedTableRow): string {
|
|
107
108
|
const children: string[] = [];
|
|
108
109
|
if (row.cnfStyle) {
|
|
109
|
-
children.push(`<w:cnfStyle w:val="${
|
|
110
|
+
children.push(`<w:cnfStyle w:val="${escapeXmlAttribute(row.cnfStyle)}"/>`);
|
|
110
111
|
}
|
|
111
112
|
if (row.cantSplit !== undefined) {
|
|
112
113
|
children.push(row.cantSplit ? `<w:cantSplit/>` : `<w:cantSplit w:val="0"/>`);
|
|
@@ -131,7 +132,7 @@ function ensureCellProperties(cell: ParsedTableCell): string {
|
|
|
131
132
|
|
|
132
133
|
const children: string[] = [];
|
|
133
134
|
if (cell.cnfStyle) {
|
|
134
|
-
children.push(`<w:cnfStyle w:val="${
|
|
135
|
+
children.push(`<w:cnfStyle w:val="${escapeXmlAttribute(cell.cnfStyle)}"/>`);
|
|
135
136
|
}
|
|
136
137
|
if (cell.width) {
|
|
137
138
|
children.push(serializeWidth("tcW", cell.width));
|
|
@@ -174,15 +175,15 @@ function ensureCellProperties(cell: ParsedTableCell): string {
|
|
|
174
175
|
}
|
|
175
176
|
|
|
176
177
|
function serializeWidth(element: string, width: ParsedTableWidth): string {
|
|
177
|
-
return `<w:${element} w:w="${twip(width.value)}" w:type="${width.type}"/>`;
|
|
178
|
+
return `<w:${element} w:w="${twip(width.value)}" w:type="${escapeXmlAttribute(width.type)}"/>`;
|
|
178
179
|
}
|
|
179
180
|
|
|
180
181
|
function serializeBorderSpec(element: string, spec: ParsedBorderSpec): string {
|
|
181
182
|
const attrs: string[] = [];
|
|
182
|
-
if (spec.value) attrs.push(`w:val="${spec.value}"`);
|
|
183
|
+
if (spec.value) attrs.push(`w:val="${escapeXmlAttribute(spec.value)}"`);
|
|
183
184
|
if (spec.size !== undefined) attrs.push(`w:sz="${twip(spec.size)}"`);
|
|
184
185
|
if (spec.space !== undefined) attrs.push(`w:space="${twip(spec.space)}"`);
|
|
185
|
-
if (spec.color) attrs.push(`w:color="${spec.color}"`);
|
|
186
|
+
if (spec.color) attrs.push(`w:color="${escapeXmlAttribute(spec.color)}"`);
|
|
186
187
|
const attrsStr = attrs.length > 0 ? ` ${attrs.join(" ")}` : "";
|
|
187
188
|
return `<w:${element}${attrsStr}/>`;
|
|
188
189
|
}
|
|
@@ -240,11 +241,3 @@ function serializeTableCellMargins(margins: ParsedCellMargins): string {
|
|
|
240
241
|
if (margins.right !== undefined) parts.push(`<w:right w:w="${twip(margins.right)}" w:type="dxa"/>`);
|
|
241
242
|
return parts.join("");
|
|
242
243
|
}
|
|
243
|
-
|
|
244
|
-
function escapeAttribute(value: string): string {
|
|
245
|
-
return value
|
|
246
|
-
.replace(/&/gu, "&")
|
|
247
|
-
.replace(/"/gu, """)
|
|
248
|
-
.replace(/</gu, "<")
|
|
249
|
-
.replace(/>/gu, ">");
|
|
250
|
-
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { twip } from "./twip.ts";
|
|
2
|
+
import { escapeXmlAttribute } from "./escape-xml-attribute.ts";
|
|
2
3
|
|
|
3
4
|
interface TableWidthLike {
|
|
4
5
|
value: number;
|
|
@@ -233,31 +234,31 @@ function stripKnownProperties(xml: string, stripSpec: PropertyStripSpec): string
|
|
|
233
234
|
function buildTablePropertiesInnerXml(table: TablePropertiesLike): string {
|
|
234
235
|
const children: string[] = [];
|
|
235
236
|
if (table.styleId) {
|
|
236
|
-
children.push(`<w:tblStyle w:val="${
|
|
237
|
+
children.push(`<w:tblStyle w:val="${escapeXmlAttribute(table.styleId)}"/>`);
|
|
237
238
|
}
|
|
238
239
|
if (table.width) {
|
|
239
240
|
children.push(serializeWidth("tblW", table.width));
|
|
240
241
|
}
|
|
241
242
|
if (table.alignment) {
|
|
242
|
-
children.push(`<w:jc w:val="${
|
|
243
|
+
children.push(`<w:jc w:val="${escapeXmlAttribute(table.alignment)}"/>`);
|
|
243
244
|
}
|
|
244
245
|
if (table.indent) {
|
|
245
|
-
children.push(`<w:tblInd w:w="${table.indent.value}" w:type="${
|
|
246
|
+
children.push(`<w:tblInd w:w="${table.indent.value}" w:type="${escapeXmlAttribute(table.indent.type)}"/>`);
|
|
246
247
|
}
|
|
247
248
|
if (table.layoutMode) {
|
|
248
|
-
children.push(`<w:tblLayout w:type="${
|
|
249
|
+
children.push(`<w:tblLayout w:type="${escapeXmlAttribute(table.layoutMode)}"/>`);
|
|
249
250
|
}
|
|
250
251
|
if (table.cellSpacing) {
|
|
251
|
-
children.push(`<w:tblCellSpacing w:w="${table.cellSpacing.value}" w:type="${
|
|
252
|
+
children.push(`<w:tblCellSpacing w:w="${table.cellSpacing.value}" w:type="${escapeXmlAttribute(table.cellSpacing.type)}"/>`);
|
|
252
253
|
}
|
|
253
254
|
if (table.bidiVisual !== undefined) {
|
|
254
255
|
children.push(table.bidiVisual ? `<w:bidiVisual/>` : `<w:bidiVisual w:val="0"/>`);
|
|
255
256
|
}
|
|
256
257
|
if (table.caption !== undefined) {
|
|
257
|
-
children.push(`<w:tblCaption w:val="${
|
|
258
|
+
children.push(`<w:tblCaption w:val="${escapeXmlAttribute(table.caption)}"/>`);
|
|
258
259
|
}
|
|
259
260
|
if (table.description !== undefined) {
|
|
260
|
-
children.push(`<w:tblDescription w:val="${
|
|
261
|
+
children.push(`<w:tblDescription w:val="${escapeXmlAttribute(table.description)}"/>`);
|
|
261
262
|
}
|
|
262
263
|
if (table.floating) {
|
|
263
264
|
const floatingXml = serializeTableFloating(table.floating);
|
|
@@ -286,11 +287,11 @@ function buildTablePropertiesInnerXml(table: TablePropertiesLike): string {
|
|
|
286
287
|
|
|
287
288
|
function serializeTableFloating(floating: TableFloatingPropertiesLike): string {
|
|
288
289
|
const attrs: string[] = [];
|
|
289
|
-
if (floating.horizontalAnchor) attrs.push(`w:horzAnchor="${
|
|
290
|
-
if (floating.verticalAnchor) attrs.push(`w:vertAnchor="${
|
|
291
|
-
if (floating.horizontalAlign) attrs.push(`w:tblpXSpec="${
|
|
290
|
+
if (floating.horizontalAnchor) attrs.push(`w:horzAnchor="${escapeXmlAttribute(floating.horizontalAnchor)}"`);
|
|
291
|
+
if (floating.verticalAnchor) attrs.push(`w:vertAnchor="${escapeXmlAttribute(floating.verticalAnchor)}"`);
|
|
292
|
+
if (floating.horizontalAlign) attrs.push(`w:tblpXSpec="${escapeXmlAttribute(floating.horizontalAlign)}"`);
|
|
292
293
|
if (floating.horizontalOffset !== undefined) attrs.push(`w:tblpX="${floating.horizontalOffset}"`);
|
|
293
|
-
if (floating.verticalAlign) attrs.push(`w:tblpYSpec="${
|
|
294
|
+
if (floating.verticalAlign) attrs.push(`w:tblpYSpec="${escapeXmlAttribute(floating.verticalAlign)}"`);
|
|
294
295
|
if (floating.verticalOffset !== undefined) attrs.push(`w:tblpY="${floating.verticalOffset}"`);
|
|
295
296
|
if (floating.leftFromText !== undefined) attrs.push(`w:leftFromText="${floating.leftFromText}"`);
|
|
296
297
|
if (floating.rightFromText !== undefined) attrs.push(`w:rightFromText="${floating.rightFromText}"`);
|
|
@@ -306,7 +307,7 @@ function serializeTableFloating(floating: TableFloatingPropertiesLike): string {
|
|
|
306
307
|
function buildTableRowPropertiesInnerXml(row: TableRowPropertiesLike): string {
|
|
307
308
|
const children: string[] = [];
|
|
308
309
|
if (row.cnfStyle) {
|
|
309
|
-
children.push(`<w:cnfStyle w:val="${
|
|
310
|
+
children.push(`<w:cnfStyle w:val="${escapeXmlAttribute(row.cnfStyle)}"/>`);
|
|
310
311
|
}
|
|
311
312
|
if (row.gridBefore !== undefined) {
|
|
312
313
|
children.push(`<w:gridBefore w:val="${twip(row.gridBefore)}"/>`);
|
|
@@ -328,7 +329,7 @@ function buildTableRowPropertiesInnerXml(row: TableRowPropertiesLike): string {
|
|
|
328
329
|
children.push(row.cantSplit ? `<w:cantSplit/>` : `<w:cantSplit w:val="0"/>`);
|
|
329
330
|
}
|
|
330
331
|
if (row.height !== undefined) {
|
|
331
|
-
const hRuleAttr = row.heightRule ? ` w:hRule="${
|
|
332
|
+
const hRuleAttr = row.heightRule ? ` w:hRule="${escapeXmlAttribute(row.heightRule)}"` : "";
|
|
332
333
|
children.push(`<w:trHeight w:val="${twip(row.height)}"${hRuleAttr}/>`);
|
|
333
334
|
}
|
|
334
335
|
// ST_OnOff element (A.3):
|
|
@@ -343,7 +344,7 @@ function buildTableRowPropertiesInnerXml(row: TableRowPropertiesLike): string {
|
|
|
343
344
|
children.push(`<w:tblHeader w:val="false"/>`);
|
|
344
345
|
}
|
|
345
346
|
if (row.horizontalAlignment) {
|
|
346
|
-
children.push(`<w:jc w:val="${
|
|
347
|
+
children.push(`<w:jc w:val="${escapeXmlAttribute(row.horizontalAlignment)}"/>`);
|
|
347
348
|
}
|
|
348
349
|
return children.join("");
|
|
349
350
|
}
|
|
@@ -351,7 +352,7 @@ function buildTableRowPropertiesInnerXml(row: TableRowPropertiesLike): string {
|
|
|
351
352
|
function buildTableCellPropertiesInnerXml(cell: TableCellPropertiesLike): string {
|
|
352
353
|
const children: string[] = [];
|
|
353
354
|
if (cell.cnfStyle) {
|
|
354
|
-
children.push(`<w:cnfStyle w:val="${
|
|
355
|
+
children.push(`<w:cnfStyle w:val="${escapeXmlAttribute(cell.cnfStyle)}"/>`);
|
|
355
356
|
}
|
|
356
357
|
if (cell.width) {
|
|
357
358
|
children.push(serializeWidth("tcW", cell.width));
|
|
@@ -391,10 +392,10 @@ function buildTableCellPropertiesInnerXml(cell: TableCellPropertiesLike): string
|
|
|
391
392
|
children.push(cell.fitText ? `<w:tcFitText/>` : `<w:tcFitText w:val="0"/>`);
|
|
392
393
|
}
|
|
393
394
|
if (cell.textDirection) {
|
|
394
|
-
children.push(`<w:textDirection w:val="${
|
|
395
|
+
children.push(`<w:textDirection w:val="${escapeXmlAttribute(cell.textDirection)}"/>`);
|
|
395
396
|
}
|
|
396
397
|
if (cell.verticalAlign) {
|
|
397
|
-
children.push(`<w:vAlign w:val="${
|
|
398
|
+
children.push(`<w:vAlign w:val="${escapeXmlAttribute(cell.verticalAlign)}"/>`);
|
|
398
399
|
}
|
|
399
400
|
return children.join("");
|
|
400
401
|
}
|
|
@@ -402,7 +403,7 @@ function buildTableCellPropertiesInnerXml(cell: TableCellPropertiesLike): string
|
|
|
402
403
|
function serializeWidth(elementName: "tblW" | "tcW", width: TableWidthLike): string {
|
|
403
404
|
// OOXML allows w:w to be percentage (pct) or twentieths-of-a-percent too, but
|
|
404
405
|
// both are integer-typed in the schema. Always round at the authoring edge.
|
|
405
|
-
return `<w:${elementName} w:w="${twip(width.value)}" w:type="${
|
|
406
|
+
return `<w:${elementName} w:w="${twip(width.value)}" w:type="${escapeXmlAttribute(width.type)}"/>`;
|
|
406
407
|
}
|
|
407
408
|
|
|
408
409
|
function serializeBorders(borders: TableBordersLike): string {
|
|
@@ -415,10 +416,10 @@ function serializeBorders(borders: TableBordersLike): string {
|
|
|
415
416
|
|
|
416
417
|
function serializeBorderSpec(elementName: string, border: BorderSpecLike): string {
|
|
417
418
|
const attrs: string[] = [];
|
|
418
|
-
if (border.value) attrs.push(`w:val="${
|
|
419
|
+
if (border.value) attrs.push(`w:val="${escapeXmlAttribute(border.value)}"`);
|
|
419
420
|
if (border.size !== undefined) attrs.push(`w:sz="${twip(border.size)}"`);
|
|
420
421
|
if (border.space !== undefined) attrs.push(`w:space="${twip(border.space)}"`);
|
|
421
|
-
if (border.color) attrs.push(`w:color="${
|
|
422
|
+
if (border.color) attrs.push(`w:color="${escapeXmlAttribute(border.color)}"`);
|
|
422
423
|
return attrs.length > 0 ? `<w:${elementName} ${attrs.join(" ")}/>` : "";
|
|
423
424
|
}
|
|
424
425
|
|
|
@@ -434,7 +435,7 @@ function serializeTableCellMargins(margins: TableCellMarginsLike): string {
|
|
|
434
435
|
function serializeTableLook(tblLook: TableLookLike): string {
|
|
435
436
|
const attrs: string[] = [];
|
|
436
437
|
if (tblLook.val) {
|
|
437
|
-
attrs.push(`w:val="${
|
|
438
|
+
attrs.push(`w:val="${escapeXmlAttribute(tblLook.val)}"`);
|
|
438
439
|
}
|
|
439
440
|
for (const [key, attr] of [
|
|
440
441
|
["firstRow", "w:firstRow"],
|
|
@@ -457,16 +458,8 @@ function serializeTableLook(tblLook: TableLookLike): string {
|
|
|
457
458
|
|
|
458
459
|
function serializeCellShading(shading: CellShadingLike): string {
|
|
459
460
|
const attrs: string[] = [];
|
|
460
|
-
if (shading.val) attrs.push(`w:val="${
|
|
461
|
-
if (shading.color) attrs.push(`w:color="${
|
|
462
|
-
if (shading.fill) attrs.push(`w:fill="${
|
|
461
|
+
if (shading.val) attrs.push(`w:val="${escapeXmlAttribute(shading.val)}"`);
|
|
462
|
+
if (shading.color) attrs.push(`w:color="${escapeXmlAttribute(shading.color)}"`);
|
|
463
|
+
if (shading.fill) attrs.push(`w:fill="${escapeXmlAttribute(shading.fill)}"`);
|
|
463
464
|
return attrs.length > 0 ? `<w:shd ${attrs.join(" ")}/>` : "";
|
|
464
465
|
}
|
|
465
|
-
|
|
466
|
-
function escapeAttribute(value: string): string {
|
|
467
|
-
return value
|
|
468
|
-
.replace(/&/gu, "&")
|
|
469
|
-
.replace(/"/gu, """)
|
|
470
|
-
.replace(/</gu, "<")
|
|
471
|
-
.replace(/>/gu, ">");
|
|
472
|
-
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ExternalCustody,
|
|
3
|
+
ExternalCustodyResolver,
|
|
4
|
+
ExternalCustodyRestoredContent,
|
|
5
|
+
} from "../../api/external-custody-types.ts";
|
|
6
|
+
|
|
7
|
+
export type ReimportResult =
|
|
8
|
+
| { outcome: "skipped" /* no custody attached */ }
|
|
9
|
+
| { outcome: "resolver_missing" /* custody present, no host resolver */ }
|
|
10
|
+
| { outcome: "tampered" /* incoming doc body hash ≠ originContentHash */ }
|
|
11
|
+
| { outcome: "deferred" /* resolver returned undefined; keep custody for retry */ }
|
|
12
|
+
| { outcome: "restored"; content: ExternalCustodyRestoredContent };
|
|
13
|
+
|
|
14
|
+
export interface MaybeRestoreArgs {
|
|
15
|
+
custody: ExternalCustody | undefined;
|
|
16
|
+
resolver: ExternalCustodyResolver | undefined;
|
|
17
|
+
/** sha256:{hex} of canonicalized word/document.xml of the incoming docx. */
|
|
18
|
+
incomingDocxHash: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Re-import pipeline. Classifies the call into a closed outcome set so
|
|
23
|
+
* the caller can decide how to merge the restored content back into
|
|
24
|
+
* the runtime state.
|
|
25
|
+
*/
|
|
26
|
+
export async function maybeRestoreFromExternalCustody(
|
|
27
|
+
args: MaybeRestoreArgs,
|
|
28
|
+
): Promise<ReimportResult> {
|
|
29
|
+
if (!args.custody) return { outcome: "skipped" };
|
|
30
|
+
if (!args.resolver) return { outcome: "resolver_missing" };
|
|
31
|
+
if (args.custody.originContentHash !== args.incomingDocxHash) {
|
|
32
|
+
return { outcome: "tampered" };
|
|
33
|
+
}
|
|
34
|
+
const restored = await args.resolver.restore({
|
|
35
|
+
custodyId: args.custody.custodyId,
|
|
36
|
+
originContentHash: args.custody.originContentHash,
|
|
37
|
+
});
|
|
38
|
+
if (!restored) return { outcome: "deferred" };
|
|
39
|
+
return { outcome: "restored", content: restored };
|
|
40
|
+
}
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal XML helpers shared by the `bw:` round-trip builders +
|
|
3
|
+
* parsers. Intentionally not a general-purpose parser — only covers
|
|
4
|
+
* the shapes the bw schema uses: elements with attributes, text +
|
|
5
|
+
* CDATA content, no processing instructions, no mixed namespaces.
|
|
6
|
+
*
|
|
7
|
+
* The canonicalizer in `canonicalize-payload.ts` has its own inline
|
|
8
|
+
* parser because its contract is stricter (attribute sort, sort-key
|
|
9
|
+
* tables). These helpers are for conventional build/parse work where
|
|
10
|
+
* we just need the tree.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export interface BwElement {
|
|
14
|
+
kind: "element";
|
|
15
|
+
name: string;
|
|
16
|
+
attributes: Record<string, string>;
|
|
17
|
+
children: BwNode[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface BwText {
|
|
21
|
+
kind: "text";
|
|
22
|
+
text: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type BwNode = BwElement | BwText;
|
|
26
|
+
|
|
27
|
+
export function parseBwXml(src: string): BwElement {
|
|
28
|
+
let i = 0;
|
|
29
|
+
skipPrologAndWs();
|
|
30
|
+
const root = readElement();
|
|
31
|
+
if (!root) throw new Error("bw-xml: no root element");
|
|
32
|
+
return root;
|
|
33
|
+
|
|
34
|
+
function skipPrologAndWs(): void {
|
|
35
|
+
while (i < src.length) {
|
|
36
|
+
if (src.startsWith("<?", i)) {
|
|
37
|
+
const end = src.indexOf("?>", i);
|
|
38
|
+
if (end < 0) throw new Error("bw-xml: unterminated prolog");
|
|
39
|
+
i = end + 2;
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
if (src.startsWith("<!--", i)) {
|
|
43
|
+
const end = src.indexOf("-->", i);
|
|
44
|
+
if (end < 0) throw new Error("bw-xml: unterminated comment");
|
|
45
|
+
i = end + 3;
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (/\s/.test(src[i]!)) {
|
|
49
|
+
i += 1;
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function readElement(): BwElement | null {
|
|
57
|
+
while (i < src.length) {
|
|
58
|
+
if (src.startsWith("<!--", i)) {
|
|
59
|
+
const end = src.indexOf("-->", i);
|
|
60
|
+
if (end < 0) throw new Error("bw-xml: unterminated comment");
|
|
61
|
+
i = end + 3;
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (/\s/.test(src[i]!)) {
|
|
65
|
+
i += 1;
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
if (src[i] !== "<") return null;
|
|
71
|
+
if (src.startsWith("</", i)) return null;
|
|
72
|
+
i += 1;
|
|
73
|
+
|
|
74
|
+
const nameStart = i;
|
|
75
|
+
while (i < src.length && !/[\s/>]/.test(src[i]!)) i += 1;
|
|
76
|
+
const name = src.slice(nameStart, i);
|
|
77
|
+
|
|
78
|
+
const attrs: Record<string, string> = {};
|
|
79
|
+
while (i < src.length) {
|
|
80
|
+
while (i < src.length && /\s/.test(src[i]!)) i += 1;
|
|
81
|
+
if (src[i] === "/" || src[i] === ">") break;
|
|
82
|
+
const aStart = i;
|
|
83
|
+
while (i < src.length && src[i] !== "=" && !/\s/.test(src[i]!)) i += 1;
|
|
84
|
+
const aName = src.slice(aStart, i);
|
|
85
|
+
while (i < src.length && /\s/.test(src[i]!)) i += 1;
|
|
86
|
+
if (src[i] !== "=") {
|
|
87
|
+
attrs[aName] = "";
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
i += 1;
|
|
91
|
+
while (i < src.length && /\s/.test(src[i]!)) i += 1;
|
|
92
|
+
const quote = src[i];
|
|
93
|
+
if (quote !== '"' && quote !== "'") {
|
|
94
|
+
throw new Error(`bw-xml: unquoted attr at ${i}`);
|
|
95
|
+
}
|
|
96
|
+
i += 1;
|
|
97
|
+
const vStart = i;
|
|
98
|
+
while (i < src.length && src[i] !== quote) i += 1;
|
|
99
|
+
attrs[aName] = xmlDecode(src.slice(vStart, i));
|
|
100
|
+
i += 1;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (src[i] === "/") {
|
|
104
|
+
i += 1;
|
|
105
|
+
if (src[i] !== ">") throw new Error("bw-xml: bad self-close");
|
|
106
|
+
i += 1;
|
|
107
|
+
return { kind: "element", name, attributes: attrs, children: [] };
|
|
108
|
+
}
|
|
109
|
+
if (src[i] !== ">") throw new Error("bw-xml: expected >");
|
|
110
|
+
i += 1;
|
|
111
|
+
|
|
112
|
+
const children: BwNode[] = [];
|
|
113
|
+
while (i < src.length) {
|
|
114
|
+
if (src.startsWith("</", i)) {
|
|
115
|
+
i += 2;
|
|
116
|
+
const endStart = i;
|
|
117
|
+
while (i < src.length && src[i] !== ">") i += 1;
|
|
118
|
+
const endName = src.slice(endStart, i).trim();
|
|
119
|
+
if (endName !== name) {
|
|
120
|
+
throw new Error(
|
|
121
|
+
`bw-xml: mismatched close: opened <${name}> got </${endName}>`,
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
i += 1;
|
|
125
|
+
return { kind: "element", name, attributes: attrs, children };
|
|
126
|
+
}
|
|
127
|
+
if (src.startsWith("<!--", i)) {
|
|
128
|
+
const end = src.indexOf("-->", i);
|
|
129
|
+
if (end < 0) throw new Error("bw-xml: unterminated comment");
|
|
130
|
+
i = end + 3;
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
if (src.startsWith("<![CDATA[", i)) {
|
|
134
|
+
const end = src.indexOf("]]>", i);
|
|
135
|
+
if (end < 0) throw new Error("bw-xml: unterminated CDATA");
|
|
136
|
+
children.push({ kind: "text", text: src.slice(i + 9, end) });
|
|
137
|
+
i = end + 3;
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
if (src[i] === "<") {
|
|
141
|
+
const child = readElement();
|
|
142
|
+
if (child) children.push(child);
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
const textStart = i;
|
|
146
|
+
while (i < src.length && src[i] !== "<") i += 1;
|
|
147
|
+
const raw = src.slice(textStart, i);
|
|
148
|
+
if (raw.length > 0) {
|
|
149
|
+
children.push({ kind: "text", text: xmlDecode(raw) });
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
throw new Error(`bw-xml: unterminated <${name}>`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ----- build helpers --------------------------------------------------------
|
|
157
|
+
|
|
158
|
+
export function renderElement(
|
|
159
|
+
name: string,
|
|
160
|
+
attrs: Record<string, string | undefined>,
|
|
161
|
+
children: readonly string[] = [],
|
|
162
|
+
): string {
|
|
163
|
+
const pairs: string[] = [];
|
|
164
|
+
for (const [k, v] of Object.entries(attrs)) {
|
|
165
|
+
if (v === undefined || v === "") continue;
|
|
166
|
+
pairs.push(`${k}="${xmlEncode(v)}"`);
|
|
167
|
+
}
|
|
168
|
+
const head = pairs.length ? `${name} ${pairs.join(" ")}` : name;
|
|
169
|
+
const body = children.filter((c) => c.length > 0).join("");
|
|
170
|
+
if (body.length === 0) return `<${head}/>`;
|
|
171
|
+
return `<${head}>${body}</${name}>`;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function renderText(text: string): string {
|
|
175
|
+
return xmlEncode(text);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function renderCdata(text: string): string {
|
|
179
|
+
const safe = text.replace(/\]\]>/g, "]]]]><![CDATA[>");
|
|
180
|
+
return `<![CDATA[${safe}]]>`;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ----- element traversal helpers -------------------------------------------
|
|
184
|
+
|
|
185
|
+
export function childrenOf(el: BwElement, localName: string): BwElement[] {
|
|
186
|
+
const out: BwElement[] = [];
|
|
187
|
+
for (const child of el.children) {
|
|
188
|
+
if (child.kind === "element" && stripNs(child.name) === localName) {
|
|
189
|
+
out.push(child);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return out;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function firstChild(
|
|
196
|
+
el: BwElement,
|
|
197
|
+
localName: string,
|
|
198
|
+
): BwElement | undefined {
|
|
199
|
+
for (const child of el.children) {
|
|
200
|
+
if (child.kind === "element" && stripNs(child.name) === localName) {
|
|
201
|
+
return child;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return undefined;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export function textOf(el: BwElement): string {
|
|
208
|
+
return el.children
|
|
209
|
+
.map((c) => (c.kind === "text" ? c.text : ""))
|
|
210
|
+
.join("");
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export function stripNs(qname: string): string {
|
|
214
|
+
const colon = qname.indexOf(":");
|
|
215
|
+
return colon < 0 ? qname : qname.slice(colon + 1);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export function attrNumber(
|
|
219
|
+
value: string | undefined,
|
|
220
|
+
): number | undefined {
|
|
221
|
+
if (value === undefined || value === "") return undefined;
|
|
222
|
+
const n = Number(value);
|
|
223
|
+
return Number.isFinite(n) ? n : undefined;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// ----- encoding -------------------------------------------------------------
|
|
227
|
+
|
|
228
|
+
export function xmlEncode(text: string): string {
|
|
229
|
+
return text
|
|
230
|
+
.replace(/&/g, "&")
|
|
231
|
+
.replace(/</g, "<")
|
|
232
|
+
.replace(/>/g, ">")
|
|
233
|
+
.replace(/"/g, """)
|
|
234
|
+
.replace(/'/g, "'");
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export function xmlDecode(text: string): string {
|
|
238
|
+
return text
|
|
239
|
+
.replace(/</g, "<")
|
|
240
|
+
.replace(/>/g, ">")
|
|
241
|
+
.replace(/"/g, '"')
|
|
242
|
+
.replace(/'/g, "'")
|
|
243
|
+
.replace(/&/g, "&");
|
|
244
|
+
}
|