@beyondwork/docx-react-component 1.0.17 → 1.0.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -2
- package/package.json +32 -34
- package/src/api/README.md +5 -1
- package/src/api/public-types.ts +374 -4
- package/src/api/session-state.ts +58 -0
- package/src/core/commands/formatting-commands.ts +1 -0
- package/src/core/commands/image-commands.ts +147 -0
- package/src/core/commands/index.ts +5 -1
- package/src/core/commands/list-commands.ts +231 -36
- package/src/core/commands/paragraph-layout-commands.ts +339 -0
- package/src/core/commands/section-layout-commands.ts +680 -0
- package/src/core/commands/style-commands.ts +262 -0
- package/src/core/search/search-text.ts +329 -0
- package/src/core/selection/mapping.ts +41 -0
- package/src/core/state/editor-state.ts +1 -1
- package/src/index.ts +30 -0
- package/src/io/docx-session.ts +260 -39
- package/src/io/export/serialize-main-document.ts +202 -5
- package/src/io/export/serialize-numbering.ts +28 -7
- package/src/io/normalize/normalize-text.ts +63 -25
- package/src/io/ooxml/numbering-sentinels.ts +44 -0
- package/src/io/ooxml/parse-footnotes.ts +212 -20
- package/src/io/ooxml/parse-headers-footers.ts +229 -25
- package/src/io/ooxml/parse-inline-media.ts +16 -0
- package/src/io/ooxml/parse-main-document.ts +411 -6
- package/src/io/ooxml/parse-numbering.ts +7 -0
- package/src/io/ooxml/parse-settings.ts +184 -0
- package/src/io/ooxml/parse-shapes.ts +25 -0
- package/src/io/ooxml/parse-styles.ts +463 -0
- package/src/io/ooxml/parse-theme.ts +32 -0
- package/src/model/canonical-document.ts +133 -3
- package/src/model/cds-1.0.0.ts +13 -0
- package/src/model/snapshot.ts +2 -1
- package/src/runtime/document-layout.ts +332 -0
- package/src/runtime/document-navigation.ts +564 -0
- package/src/runtime/document-runtime.ts +265 -35
- package/src/runtime/document-search.ts +145 -0
- package/src/runtime/numbering-prefix.ts +47 -26
- package/src/runtime/page-layout-estimation.ts +212 -0
- package/src/runtime/read-only-diagnostics-runtime.ts +1 -0
- package/src/runtime/session-capabilities.ts +2 -0
- package/src/runtime/story-context.ts +164 -0
- package/src/runtime/story-targeting.ts +162 -0
- package/src/runtime/surface-projection.ts +239 -12
- package/src/runtime/table-schema.ts +87 -5
- package/src/runtime/view-state.ts +459 -0
- package/src/ui/WordReviewEditor.tsx +1902 -312
- package/src/ui/browser-export.ts +52 -0
- package/src/ui/headless/preserve-editor-selection.ts +5 -0
- package/src/ui/headless/selection-helpers.ts +20 -0
- package/src/ui/headless/selection-toolbar-model.ts +22 -0
- package/src/ui/headless/use-editor-keyboard.ts +6 -1
- package/src/ui-tailwind/chrome/tw-page-ruler.tsx +386 -0
- package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +125 -14
- package/src/ui-tailwind/editor-surface/perf-probe.ts +107 -0
- package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +45 -6
- package/src/ui-tailwind/editor-surface/pm-contextual-ui.ts +31 -0
- package/src/ui-tailwind/editor-surface/pm-position-map.ts +2 -2
- package/src/ui-tailwind/editor-surface/pm-schema.ts +47 -5
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +95 -22
- package/src/ui-tailwind/editor-surface/search-plugin.ts +19 -68
- package/src/ui-tailwind/editor-surface/tw-inline-token.tsx +11 -0
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +394 -77
- package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +0 -1
- package/src/ui-tailwind/index.ts +2 -1
- package/src/ui-tailwind/review/tw-comment-sidebar.tsx +277 -147
- package/src/ui-tailwind/review/tw-review-rail.tsx +6 -6
- package/src/ui-tailwind/theme/editor-theme.css +123 -0
- package/src/ui-tailwind/toolbar/tw-toolbar-icon-button.tsx +4 -0
- package/src/ui-tailwind/toolbar/tw-toolbar.tsx +291 -12
- package/src/ui-tailwind/tw-review-workspace.tsx +926 -27
- package/src/validation/compatibility-engine.ts +92 -20
- package/src/validation/diagnostics.ts +1 -0
- package/src/validation/docx-comment-proof.ts +487 -0
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type {
|
|
2
|
+
EditorStoryTarget,
|
|
2
3
|
EditorSurfaceSnapshot,
|
|
4
|
+
SecondaryStorySurface,
|
|
3
5
|
SurfaceBlockSnapshot,
|
|
4
6
|
SurfaceInlineSegment,
|
|
5
7
|
SurfaceTableCellSnapshot,
|
|
@@ -20,6 +22,7 @@ import type {
|
|
|
20
22
|
SdtNode,
|
|
21
23
|
ShapeNode,
|
|
22
24
|
SmartArtPreviewNode,
|
|
25
|
+
TableCellBorders,
|
|
23
26
|
TableNode,
|
|
24
27
|
TextMark,
|
|
25
28
|
VmlShapeNode,
|
|
@@ -29,10 +32,16 @@ import {
|
|
|
29
32
|
describeOpaqueFragment,
|
|
30
33
|
getOpaqueFragment,
|
|
31
34
|
} from "../preservation/store.ts";
|
|
35
|
+
import { getStoryBlocks } from "./story-targeting.ts";
|
|
32
36
|
import {
|
|
33
37
|
createNumberingPrefixResolver,
|
|
34
38
|
type NumberingPrefixResolver,
|
|
35
39
|
} from "./numbering-prefix.ts";
|
|
40
|
+
import {
|
|
41
|
+
collectSectionContexts,
|
|
42
|
+
findHeaderFooterDocumentEntry,
|
|
43
|
+
resolveSectionVariants,
|
|
44
|
+
} from "./story-context.ts";
|
|
36
45
|
|
|
37
46
|
interface ParagraphAccumulator {
|
|
38
47
|
blockId: string;
|
|
@@ -42,14 +51,19 @@ interface ParagraphAccumulator {
|
|
|
42
51
|
styleId?: string;
|
|
43
52
|
numbering?: ParagraphNode["numbering"];
|
|
44
53
|
numberingPrefix?: string;
|
|
54
|
+
numberingSuffix?: "tab" | "space" | "nothing";
|
|
45
55
|
segments: SurfaceInlineSegment[];
|
|
46
56
|
}
|
|
47
57
|
|
|
48
58
|
export function createEditorSurfaceSnapshot(
|
|
49
59
|
document: CanonicalDocumentEnvelope,
|
|
50
60
|
_selection: SelectionSnapshot,
|
|
61
|
+
activeStory: EditorStoryTarget = { kind: "main" },
|
|
51
62
|
): EditorSurfaceSnapshot {
|
|
52
|
-
const root = normalizeDocumentRoot(
|
|
63
|
+
const root = normalizeDocumentRoot({
|
|
64
|
+
type: "doc",
|
|
65
|
+
children: [...getStoryBlocks(document, activeStory)],
|
|
66
|
+
});
|
|
53
67
|
const blocks: SurfaceBlockSnapshot[] = [];
|
|
54
68
|
const lockedFragmentIds: string[] = [];
|
|
55
69
|
const numberingPrefixResolver = createNumberingPrefixResolver(document.numbering);
|
|
@@ -79,13 +93,14 @@ export function createEditorSurfaceSnapshot(
|
|
|
79
93
|
}
|
|
80
94
|
}
|
|
81
95
|
|
|
82
|
-
|
|
96
|
+
const secondaryStories = createSecondaryStorySurfaces(document);
|
|
83
97
|
|
|
84
98
|
return {
|
|
85
99
|
storySize: cursor,
|
|
86
100
|
plainText: createPlainText(blocks),
|
|
87
101
|
blocks,
|
|
88
102
|
lockedFragmentIds,
|
|
103
|
+
secondaryStories,
|
|
89
104
|
};
|
|
90
105
|
}
|
|
91
106
|
|
|
@@ -263,16 +278,27 @@ function createTableBlock(
|
|
|
263
278
|
lockedFragmentIds.push(...result.lockedFragmentIds);
|
|
264
279
|
innerCursor = result.nextCursor;
|
|
265
280
|
}
|
|
281
|
+
const cellBorders = resolveCellBorderStyles(cell.borders);
|
|
266
282
|
cells.push({
|
|
267
283
|
gridSpan: cell.gridSpan ?? 1,
|
|
268
284
|
verticalMerge: cell.verticalMerge ?? null,
|
|
269
285
|
colspan: cell.gridSpan ?? 1,
|
|
270
286
|
rowspan: rowSpans.get(`${rowIndex}:${cellIndex}`) ?? 1,
|
|
271
287
|
...(cell.shading?.fill ? { backgroundColor: `#${cell.shading.fill}` } : {}),
|
|
288
|
+
...(cell.verticalAlign ? { verticalAlign: cell.verticalAlign } : {}),
|
|
289
|
+
...(cellBorders.borderTop ? { borderTop: cellBorders.borderTop } : {}),
|
|
290
|
+
...(cellBorders.borderRight ? { borderRight: cellBorders.borderRight } : {}),
|
|
291
|
+
...(cellBorders.borderBottom ? { borderBottom: cellBorders.borderBottom } : {}),
|
|
292
|
+
...(cellBorders.borderLeft ? { borderLeft: cellBorders.borderLeft } : {}),
|
|
272
293
|
content: cellContent,
|
|
273
294
|
});
|
|
274
295
|
}
|
|
275
|
-
rows.push({
|
|
296
|
+
rows.push({
|
|
297
|
+
cells,
|
|
298
|
+
...(row.height !== undefined ? { height: row.height } : {}),
|
|
299
|
+
...(row.heightRule ? { heightRule: row.heightRule } : {}),
|
|
300
|
+
...(row.isHeader ? { isHeader: row.isHeader } : {}),
|
|
301
|
+
});
|
|
276
302
|
}
|
|
277
303
|
|
|
278
304
|
return {
|
|
@@ -283,6 +309,8 @@ function createTableBlock(
|
|
|
283
309
|
to: innerCursor,
|
|
284
310
|
styleId: table.styleId,
|
|
285
311
|
gridColumns: table.gridColumns,
|
|
312
|
+
...(table.alignment ? { alignment: table.alignment } : {}),
|
|
313
|
+
...(table.tblLook ? { tblLook: table.tblLook } : {}),
|
|
286
314
|
rows,
|
|
287
315
|
},
|
|
288
316
|
lockedFragmentIds,
|
|
@@ -339,6 +367,23 @@ function computeTableRowSpans(table: TableNode): Map<string, number> {
|
|
|
339
367
|
return rowSpans;
|
|
340
368
|
}
|
|
341
369
|
|
|
370
|
+
function resolveCellBorderStyles(
|
|
371
|
+
borders: TableCellBorders | undefined,
|
|
372
|
+
): { borderTop?: string; borderRight?: string; borderBottom?: string; borderLeft?: string } {
|
|
373
|
+
if (!borders) return {};
|
|
374
|
+
const result: { borderTop?: string; borderRight?: string; borderBottom?: string; borderLeft?: string } = {};
|
|
375
|
+
const sides = [["top", "borderTop"], ["right", "borderRight"], ["bottom", "borderBottom"], ["left", "borderLeft"]] as const;
|
|
376
|
+
for (const [side, key] of sides) {
|
|
377
|
+
const spec = borders[side];
|
|
378
|
+
if (!spec || spec.value === "none" || spec.value === "nil") continue;
|
|
379
|
+
const width = spec.size ? `${Math.max(1, Math.round(spec.size / 8))}px` : "1px";
|
|
380
|
+
const style = spec.value === "double" ? "double" : spec.value === "dashed" || spec.value === "dashSmallGap" ? "dashed" : spec.value === "dotted" ? "dotted" : "solid";
|
|
381
|
+
const color = spec.color && spec.color !== "auto" ? `#${spec.color}` : "currentColor";
|
|
382
|
+
result[key] = `${width} ${style} ${color}`;
|
|
383
|
+
}
|
|
384
|
+
return result;
|
|
385
|
+
}
|
|
386
|
+
|
|
342
387
|
function createSdtBlock(
|
|
343
388
|
sdtIndex: number,
|
|
344
389
|
block: SdtNode,
|
|
@@ -381,6 +426,11 @@ function createSdtBlock(
|
|
|
381
426
|
...(block.properties.alias ? { alias: block.properties.alias } : {}),
|
|
382
427
|
...(block.properties.tag ? { tag: block.properties.tag } : {}),
|
|
383
428
|
...(block.properties.lock ? { lock: block.properties.lock } : {}),
|
|
429
|
+
...(block.properties.checkbox ? { checkboxChecked: block.properties.checkbox.checked } : {}),
|
|
430
|
+
...(block.properties.datePicker?.fullDate ? { dateValue: block.properties.datePicker.fullDate } : {}),
|
|
431
|
+
...(block.properties.dropdownList ? { dropdownItems: block.properties.dropdownList } : {}),
|
|
432
|
+
...(block.properties.comboBox ? { comboBoxItems: block.properties.comboBox } : {}),
|
|
433
|
+
...(block.properties.showingPlcHdr ? { showingPlcHdr: true } : {}),
|
|
384
434
|
children,
|
|
385
435
|
},
|
|
386
436
|
lockedFragmentIds,
|
|
@@ -407,10 +457,15 @@ function createParagraphBlock(
|
|
|
407
457
|
...(paragraph.styleId ? { styleId: paragraph.styleId } : {}),
|
|
408
458
|
...(paragraph.numbering ? { numbering: paragraph.numbering } : {}),
|
|
409
459
|
...(paragraph.numbering
|
|
410
|
-
? {
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
460
|
+
? (() => {
|
|
461
|
+
const detailed = numberingPrefixResolver.resolveDetailed(paragraph.numbering);
|
|
462
|
+
return detailed
|
|
463
|
+
? {
|
|
464
|
+
numberingPrefix: detailed.text,
|
|
465
|
+
...(detailed.suffix ? { numberingSuffix: detailed.suffix } : {}),
|
|
466
|
+
}
|
|
467
|
+
: {};
|
|
468
|
+
})()
|
|
414
469
|
: {}),
|
|
415
470
|
...(paragraph.alignment ? { alignment: paragraph.alignment } : {}),
|
|
416
471
|
...(paragraph.spacing ? { spacing: paragraph.spacing } : {}),
|
|
@@ -425,6 +480,7 @@ function createParagraphBlock(
|
|
|
425
480
|
...(paragraph.pageBreakBefore ? { pageBreakBefore: true } : {}),
|
|
426
481
|
...(paragraph.outlineLevel !== undefined ? { outlineLevel: paragraph.outlineLevel } : {}),
|
|
427
482
|
...(paragraph.bidi ? { bidi: true } : {}),
|
|
483
|
+
...(paragraph.suppressLineNumbers ? { suppressLineNumbers: true } : {}),
|
|
428
484
|
segments: [],
|
|
429
485
|
};
|
|
430
486
|
const lockedFragmentIds: string[] = [];
|
|
@@ -530,6 +586,7 @@ function appendInlineSegments(
|
|
|
530
586
|
preview?.detail ??
|
|
531
587
|
descriptor?.detail ??
|
|
532
588
|
"Locked whole-unit to keep unsupported inline OOXML intact through export.",
|
|
589
|
+
...(preview?.presentation ? { presentation: preview.presentation } : {}),
|
|
533
590
|
state: "locked-preserve-only",
|
|
534
591
|
});
|
|
535
592
|
return { nextCursor: start + 1, lockedFragmentIds: [node.fragmentId] };
|
|
@@ -569,11 +626,12 @@ function appendInlineSegments(
|
|
|
569
626
|
case "footnote_ref":
|
|
570
627
|
paragraph.segments.push({
|
|
571
628
|
segmentId: `${paragraph.blockId}-segment-${paragraph.segments.length}`,
|
|
572
|
-
kind: "
|
|
629
|
+
kind: "note_ref",
|
|
573
630
|
from: start,
|
|
574
631
|
to: start + 1,
|
|
575
|
-
|
|
576
|
-
|
|
632
|
+
noteKind: node.noteKind ?? "footnote",
|
|
633
|
+
noteId: node.noteId ?? "",
|
|
634
|
+
label: node.noteId ?? "*",
|
|
577
635
|
});
|
|
578
636
|
return { nextCursor: start + 1, lockedFragmentIds: [] };
|
|
579
637
|
case "field": {
|
|
@@ -643,7 +701,7 @@ function createSmartArtDetail(node: SmartArtPreviewNode): string {
|
|
|
643
701
|
}
|
|
644
702
|
|
|
645
703
|
function createShapeDetail(node: ShapeNode): string {
|
|
646
|
-
const parts = ["Shape read-only preview."];
|
|
704
|
+
const parts = [node.isTextBox ? "Text box read-only preview." : "Shape read-only preview."];
|
|
647
705
|
if (node.geometry) parts.push(`Geometry: ${node.geometry}.`);
|
|
648
706
|
if (node.text) parts.push(`Text: "${node.text}".`);
|
|
649
707
|
parts.push("Original XML preserved for export.");
|
|
@@ -717,6 +775,131 @@ function createPlainText(
|
|
|
717
775
|
return text.join("");
|
|
718
776
|
}
|
|
719
777
|
|
|
778
|
+
function createSecondaryStorySurfaces(
|
|
779
|
+
document: CanonicalDocumentEnvelope,
|
|
780
|
+
): SecondaryStorySurface[] {
|
|
781
|
+
const surfaces: SecondaryStorySurface[] = [];
|
|
782
|
+
const subParts = document.subParts;
|
|
783
|
+
if (!subParts) {
|
|
784
|
+
return surfaces;
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
const numberingPrefixResolver = createNumberingPrefixResolver(document.numbering);
|
|
788
|
+
|
|
789
|
+
for (const section of collectSectionContexts(document)) {
|
|
790
|
+
const headerVariants = resolveSectionVariants(
|
|
791
|
+
"header",
|
|
792
|
+
section.index,
|
|
793
|
+
section.properties?.headerReferences,
|
|
794
|
+
subParts.headers ?? [],
|
|
795
|
+
);
|
|
796
|
+
for (const headerVariant of headerVariants) {
|
|
797
|
+
const target: EditorStoryTarget = {
|
|
798
|
+
kind: "header",
|
|
799
|
+
relationshipId: headerVariant.relationshipId,
|
|
800
|
+
variant: headerVariant.variant,
|
|
801
|
+
sectionIndex: section.index,
|
|
802
|
+
};
|
|
803
|
+
const header = findHeaderFooterDocumentEntry(document, target);
|
|
804
|
+
if (!header) {
|
|
805
|
+
continue;
|
|
806
|
+
}
|
|
807
|
+
surfaces.push(
|
|
808
|
+
createStorySurface(
|
|
809
|
+
target,
|
|
810
|
+
`Header · ${headerVariant.variant}`,
|
|
811
|
+
header.blocks,
|
|
812
|
+
document,
|
|
813
|
+
numberingPrefixResolver,
|
|
814
|
+
),
|
|
815
|
+
);
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
const footerVariants = resolveSectionVariants(
|
|
819
|
+
"footer",
|
|
820
|
+
section.index,
|
|
821
|
+
section.properties?.footerReferences,
|
|
822
|
+
subParts.footers ?? [],
|
|
823
|
+
);
|
|
824
|
+
for (const footerVariant of footerVariants) {
|
|
825
|
+
const target: EditorStoryTarget = {
|
|
826
|
+
kind: "footer",
|
|
827
|
+
relationshipId: footerVariant.relationshipId,
|
|
828
|
+
variant: footerVariant.variant,
|
|
829
|
+
sectionIndex: section.index,
|
|
830
|
+
};
|
|
831
|
+
const footer = findHeaderFooterDocumentEntry(document, target);
|
|
832
|
+
if (!footer) {
|
|
833
|
+
continue;
|
|
834
|
+
}
|
|
835
|
+
surfaces.push(
|
|
836
|
+
createStorySurface(
|
|
837
|
+
target,
|
|
838
|
+
`Footer · ${footerVariant.variant}`,
|
|
839
|
+
footer.blocks,
|
|
840
|
+
document,
|
|
841
|
+
numberingPrefixResolver,
|
|
842
|
+
),
|
|
843
|
+
);
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
const footnotes = Object.values(subParts.footnoteCollection?.footnotes ?? {}).sort(compareNoteIds);
|
|
848
|
+
for (const note of footnotes) {
|
|
849
|
+
const target: EditorStoryTarget = { kind: "footnote", noteId: note.noteId };
|
|
850
|
+
surfaces.push(createStorySurface(target, `Footnote ${note.noteId}`, note.blocks, document, numberingPrefixResolver));
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
const endnotes = Object.values(subParts.footnoteCollection?.endnotes ?? {}).sort(compareNoteIds);
|
|
854
|
+
for (const note of endnotes) {
|
|
855
|
+
const target: EditorStoryTarget = { kind: "endnote", noteId: note.noteId };
|
|
856
|
+
surfaces.push(createStorySurface(target, `Endnote ${note.noteId}`, note.blocks, document, numberingPrefixResolver));
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
return surfaces;
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
function createStorySurface(
|
|
863
|
+
target: EditorStoryTarget,
|
|
864
|
+
label: string,
|
|
865
|
+
blocks: readonly BlockNode[],
|
|
866
|
+
document: CanonicalDocumentEnvelope,
|
|
867
|
+
numberingPrefixResolver: NumberingPrefixResolver,
|
|
868
|
+
): SecondaryStorySurface {
|
|
869
|
+
const surfaceBlocks: SurfaceBlockSnapshot[] = [];
|
|
870
|
+
let cursor = 0;
|
|
871
|
+
const counters = {
|
|
872
|
+
paragraph: 0,
|
|
873
|
+
table: 0,
|
|
874
|
+
opaque: 0,
|
|
875
|
+
sdt: 0,
|
|
876
|
+
customXml: 0,
|
|
877
|
+
altChunk: 0,
|
|
878
|
+
};
|
|
879
|
+
|
|
880
|
+
for (let index = 0; index < blocks.length; index += 1) {
|
|
881
|
+
const surfaceBlock = createSurfaceBlock(
|
|
882
|
+
blocks[index],
|
|
883
|
+
document,
|
|
884
|
+
cursor,
|
|
885
|
+
counters,
|
|
886
|
+
numberingPrefixResolver,
|
|
887
|
+
);
|
|
888
|
+
surfaceBlocks.push(surfaceBlock.block);
|
|
889
|
+
cursor = surfaceBlock.nextCursor;
|
|
890
|
+
if (index < blocks.length - 1 && blocks[index + 1]?.type === "paragraph") {
|
|
891
|
+
cursor += 1;
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
return {
|
|
896
|
+
target,
|
|
897
|
+
label,
|
|
898
|
+
storySize: cursor,
|
|
899
|
+
blocks: surfaceBlocks,
|
|
900
|
+
};
|
|
901
|
+
}
|
|
902
|
+
|
|
720
903
|
function createSecondaryStoryPreviewBlocks(
|
|
721
904
|
document: CanonicalDocumentEnvelope,
|
|
722
905
|
cursor: number,
|
|
@@ -887,7 +1070,51 @@ function toSurfaceTabStop(
|
|
|
887
1070
|
|
|
888
1071
|
function describePreservedInlinePreview(
|
|
889
1072
|
payloadReference: string,
|
|
890
|
-
): {
|
|
1073
|
+
): {
|
|
1074
|
+
label: string;
|
|
1075
|
+
detail: string;
|
|
1076
|
+
presentation?: "inline-chip" | "quiet-marker";
|
|
1077
|
+
} | null {
|
|
1078
|
+
if (/\b(?:w:)?proofErr\b/u.test(payloadReference)) {
|
|
1079
|
+
const proofType = /\bw:type="([^"]+)"/u.exec(payloadReference)?.[1];
|
|
1080
|
+
return {
|
|
1081
|
+
label: "Proofing marker",
|
|
1082
|
+
detail:
|
|
1083
|
+
proofType && proofType.trim().length > 0
|
|
1084
|
+
? `Word proofing marker (${proofType}) preserved for export safety.`
|
|
1085
|
+
: "Word proofing marker preserved for export safety.",
|
|
1086
|
+
presentation: "quiet-marker",
|
|
1087
|
+
};
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
if (/\b(?:w:)?lastRenderedPageBreak\b/u.test(payloadReference)) {
|
|
1091
|
+
return {
|
|
1092
|
+
label: "Rendered page break",
|
|
1093
|
+
detail: "Word rendered page-break marker preserved for export safety.",
|
|
1094
|
+
presentation: "quiet-marker",
|
|
1095
|
+
};
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
if (/\b(?:w:)?permStart\b/u.test(payloadReference)) {
|
|
1099
|
+
const editorGroup = /\bw:edGrp="([^"]+)"/u.exec(payloadReference)?.[1];
|
|
1100
|
+
return {
|
|
1101
|
+
label: "Protected range start",
|
|
1102
|
+
detail:
|
|
1103
|
+
editorGroup && editorGroup.trim().length > 0
|
|
1104
|
+
? `Protected range start for ${editorGroup} preserved for export safety.`
|
|
1105
|
+
: "Protected range start preserved for export safety.",
|
|
1106
|
+
presentation: "quiet-marker",
|
|
1107
|
+
};
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
if (/\b(?:w:)?permEnd\b/u.test(payloadReference)) {
|
|
1111
|
+
return {
|
|
1112
|
+
label: "Protected range end",
|
|
1113
|
+
detail: "Protected range end preserved for export safety.",
|
|
1114
|
+
presentation: "quiet-marker",
|
|
1115
|
+
};
|
|
1116
|
+
}
|
|
1117
|
+
|
|
891
1118
|
if (/\b(?:w:)?bookmarkStart\b/u.test(payloadReference)) {
|
|
892
1119
|
const name = /\bw:name="([^"]+)"/u.exec(payloadReference)?.[1];
|
|
893
1120
|
return {
|
|
@@ -11,6 +11,25 @@
|
|
|
11
11
|
|
|
12
12
|
import type { NodeSpec } from "prosemirror-model";
|
|
13
13
|
|
|
14
|
+
/** Characters that must never appear in CSS values derived from OOXML tokens. */
|
|
15
|
+
const CSS_INJECTION_RE = /[;{}()[\]\\@!]/;
|
|
16
|
+
|
|
17
|
+
/** Validate a CSS color token — allows #hex (3/6/8 digits) and named colors only. */
|
|
18
|
+
function safeCssColor(raw: string | null | undefined): string | null {
|
|
19
|
+
if (!raw) return null;
|
|
20
|
+
if (/^#[0-9A-Fa-f]{3,8}$/.test(raw)) return raw;
|
|
21
|
+
if (/^[a-zA-Z]+$/.test(raw) && raw !== "expression") return raw;
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Sanitize a composite CSS border shorthand (e.g. "1px solid #abc"). Rejects injection attempts. */
|
|
26
|
+
function safeCssBorder(raw: string | null | undefined): string | null {
|
|
27
|
+
if (!raw) return null;
|
|
28
|
+
if (CSS_INJECTION_RE.test(raw)) return null;
|
|
29
|
+
if (raw.length > 100) return null;
|
|
30
|
+
return raw;
|
|
31
|
+
}
|
|
32
|
+
|
|
14
33
|
type TableCellAttrs = {
|
|
15
34
|
colspan?: number | null;
|
|
16
35
|
rowspan?: number | null;
|
|
@@ -18,6 +37,11 @@ type TableCellAttrs = {
|
|
|
18
37
|
gridSpan?: number | null;
|
|
19
38
|
verticalMerge?: "restart" | "continue" | null;
|
|
20
39
|
backgroundColor?: string | null;
|
|
40
|
+
verticalAlign?: "top" | "center" | "bottom" | null;
|
|
41
|
+
borderTop?: string | null;
|
|
42
|
+
borderRight?: string | null;
|
|
43
|
+
borderBottom?: string | null;
|
|
44
|
+
borderLeft?: string | null;
|
|
21
45
|
};
|
|
22
46
|
|
|
23
47
|
function resolveRenderedColspan(attrs: {
|
|
@@ -55,6 +79,7 @@ function getCellAttrs(dom: HTMLElement): TableCellAttrs {
|
|
|
55
79
|
const gridSpan = gridSpanAttr ? Number.parseInt(gridSpanAttr, 10) : colspan;
|
|
56
80
|
const backgroundColor =
|
|
57
81
|
dom.getAttribute("data-cell-background") ?? dom.style.backgroundColor ?? null;
|
|
82
|
+
const verticalAlign = dom.getAttribute("data-vertical-align") as "top" | "center" | "bottom" | null;
|
|
58
83
|
|
|
59
84
|
return {
|
|
60
85
|
colspan,
|
|
@@ -66,6 +91,11 @@ function getCellAttrs(dom: HTMLElement): TableCellAttrs {
|
|
|
66
91
|
? verticalMergeAttr
|
|
67
92
|
: null,
|
|
68
93
|
backgroundColor,
|
|
94
|
+
verticalAlign: verticalAlign === "top" || verticalAlign === "center" || verticalAlign === "bottom" ? verticalAlign : null,
|
|
95
|
+
borderTop: dom.getAttribute("data-border-top"),
|
|
96
|
+
borderRight: dom.getAttribute("data-border-right"),
|
|
97
|
+
borderBottom: dom.getAttribute("data-border-bottom"),
|
|
98
|
+
borderLeft: dom.getAttribute("data-border-left"),
|
|
69
99
|
};
|
|
70
100
|
}
|
|
71
101
|
|
|
@@ -91,8 +121,28 @@ function setCellDomAttrs(nodeAttrs: TableCellAttrs, className: string): Record<s
|
|
|
91
121
|
}
|
|
92
122
|
if (nodeAttrs.backgroundColor) {
|
|
93
123
|
attrs["data-cell-background"] = nodeAttrs.backgroundColor;
|
|
94
|
-
attrs.style = `background-color: ${nodeAttrs.backgroundColor}`;
|
|
95
124
|
}
|
|
125
|
+
if (nodeAttrs.verticalAlign && nodeAttrs.verticalAlign !== "top") {
|
|
126
|
+
attrs["data-vertical-align"] = nodeAttrs.verticalAlign;
|
|
127
|
+
}
|
|
128
|
+
if (nodeAttrs.borderTop) attrs["data-border-top"] = nodeAttrs.borderTop;
|
|
129
|
+
if (nodeAttrs.borderRight) attrs["data-border-right"] = nodeAttrs.borderRight;
|
|
130
|
+
if (nodeAttrs.borderBottom) attrs["data-border-bottom"] = nodeAttrs.borderBottom;
|
|
131
|
+
if (nodeAttrs.borderLeft) attrs["data-border-left"] = nodeAttrs.borderLeft;
|
|
132
|
+
|
|
133
|
+
const styles: string[] = [];
|
|
134
|
+
const bgColor = safeCssColor(nodeAttrs.backgroundColor);
|
|
135
|
+
if (bgColor) styles.push(`background-color: ${bgColor}`);
|
|
136
|
+
if (nodeAttrs.verticalAlign) styles.push(`vertical-align: ${nodeAttrs.verticalAlign === "center" ? "middle" : nodeAttrs.verticalAlign}`);
|
|
137
|
+
const bTop = safeCssBorder(nodeAttrs.borderTop);
|
|
138
|
+
if (bTop) styles.push(`border-top: ${bTop}`);
|
|
139
|
+
const bRight = safeCssBorder(nodeAttrs.borderRight);
|
|
140
|
+
if (bRight) styles.push(`border-right: ${bRight}`);
|
|
141
|
+
const bBottom = safeCssBorder(nodeAttrs.borderBottom);
|
|
142
|
+
if (bBottom) styles.push(`border-bottom: ${bBottom}`);
|
|
143
|
+
const bLeft = safeCssBorder(nodeAttrs.borderLeft);
|
|
144
|
+
if (bLeft) styles.push(`border-left: ${bLeft}`);
|
|
145
|
+
if (styles.length > 0) attrs.style = styles.join("; ");
|
|
96
146
|
|
|
97
147
|
return attrs;
|
|
98
148
|
}
|
|
@@ -126,6 +176,11 @@ const tableCellSpecAttrs = {
|
|
|
126
176
|
rowspan: { default: 1, validate: "number" },
|
|
127
177
|
colwidth: { default: null, validate: validateColwidth },
|
|
128
178
|
backgroundColor: { default: null },
|
|
179
|
+
verticalAlign: { default: null },
|
|
180
|
+
borderTop: { default: null },
|
|
181
|
+
borderRight: { default: null },
|
|
182
|
+
borderBottom: { default: null },
|
|
183
|
+
borderLeft: { default: null },
|
|
129
184
|
} as const;
|
|
130
185
|
|
|
131
186
|
export const tableNodeSpec: NodeSpec = {
|
|
@@ -137,10 +192,29 @@ export const tableNodeSpec: NodeSpec = {
|
|
|
137
192
|
styleId: { default: null },
|
|
138
193
|
propertiesXml: { default: null },
|
|
139
194
|
gridColumns: { default: [] },
|
|
195
|
+
alignment: { default: null },
|
|
196
|
+
tblLookFirstRow: { default: false },
|
|
197
|
+
tblLookLastRow: { default: false },
|
|
198
|
+
tblLookFirstColumn: { default: false },
|
|
199
|
+
tblLookLastColumn: { default: false },
|
|
200
|
+
tblLookNoHBand: { default: false },
|
|
201
|
+
tblLookNoVBand: { default: false },
|
|
140
202
|
},
|
|
141
203
|
parseDOM: [{ tag: "table" }],
|
|
142
|
-
toDOM() {
|
|
143
|
-
|
|
204
|
+
toDOM(node) {
|
|
205
|
+
const style = node.attrs.alignment === "center"
|
|
206
|
+
? "margin-left: auto; margin-right: auto"
|
|
207
|
+
: node.attrs.alignment === "right"
|
|
208
|
+
? "margin-left: auto"
|
|
209
|
+
: undefined;
|
|
210
|
+
return [
|
|
211
|
+
"table",
|
|
212
|
+
{
|
|
213
|
+
class: "border-collapse w-full my-2 text-sm",
|
|
214
|
+
...(style ? { style } : {}),
|
|
215
|
+
},
|
|
216
|
+
["tbody", 0],
|
|
217
|
+
];
|
|
144
218
|
},
|
|
145
219
|
};
|
|
146
220
|
|
|
@@ -149,10 +223,18 @@ export const tableRowNodeSpec: NodeSpec = {
|
|
|
149
223
|
tableRole: "row",
|
|
150
224
|
attrs: {
|
|
151
225
|
propertiesXml: { default: null },
|
|
226
|
+
height: { default: null },
|
|
227
|
+
heightRule: { default: null },
|
|
228
|
+
isHeader: { default: false },
|
|
152
229
|
},
|
|
153
230
|
parseDOM: [{ tag: "tr" }],
|
|
154
|
-
toDOM() {
|
|
155
|
-
|
|
231
|
+
toDOM(node) {
|
|
232
|
+
const style = node.attrs.heightRule === "exact" && node.attrs.height
|
|
233
|
+
? `height: ${Math.round((node.attrs.height as number) / 20)}pt`
|
|
234
|
+
: node.attrs.heightRule === "atLeast" && node.attrs.height
|
|
235
|
+
? `min-height: ${Math.round((node.attrs.height as number) / 20)}pt`
|
|
236
|
+
: undefined;
|
|
237
|
+
return ["tr", { ...(style ? { style } : {}) }, 0];
|
|
156
238
|
},
|
|
157
239
|
};
|
|
158
240
|
|