@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.
Files changed (74) hide show
  1. package/README.md +8 -2
  2. package/package.json +32 -34
  3. package/src/api/README.md +5 -1
  4. package/src/api/public-types.ts +374 -4
  5. package/src/api/session-state.ts +58 -0
  6. package/src/core/commands/formatting-commands.ts +1 -0
  7. package/src/core/commands/image-commands.ts +147 -0
  8. package/src/core/commands/index.ts +5 -1
  9. package/src/core/commands/list-commands.ts +231 -36
  10. package/src/core/commands/paragraph-layout-commands.ts +339 -0
  11. package/src/core/commands/section-layout-commands.ts +680 -0
  12. package/src/core/commands/style-commands.ts +262 -0
  13. package/src/core/search/search-text.ts +329 -0
  14. package/src/core/selection/mapping.ts +41 -0
  15. package/src/core/state/editor-state.ts +1 -1
  16. package/src/index.ts +30 -0
  17. package/src/io/docx-session.ts +260 -39
  18. package/src/io/export/serialize-main-document.ts +202 -5
  19. package/src/io/export/serialize-numbering.ts +28 -7
  20. package/src/io/normalize/normalize-text.ts +63 -25
  21. package/src/io/ooxml/numbering-sentinels.ts +44 -0
  22. package/src/io/ooxml/parse-footnotes.ts +212 -20
  23. package/src/io/ooxml/parse-headers-footers.ts +229 -25
  24. package/src/io/ooxml/parse-inline-media.ts +16 -0
  25. package/src/io/ooxml/parse-main-document.ts +411 -6
  26. package/src/io/ooxml/parse-numbering.ts +7 -0
  27. package/src/io/ooxml/parse-settings.ts +184 -0
  28. package/src/io/ooxml/parse-shapes.ts +25 -0
  29. package/src/io/ooxml/parse-styles.ts +463 -0
  30. package/src/io/ooxml/parse-theme.ts +32 -0
  31. package/src/model/canonical-document.ts +133 -3
  32. package/src/model/cds-1.0.0.ts +13 -0
  33. package/src/model/snapshot.ts +2 -1
  34. package/src/runtime/document-layout.ts +332 -0
  35. package/src/runtime/document-navigation.ts +564 -0
  36. package/src/runtime/document-runtime.ts +265 -35
  37. package/src/runtime/document-search.ts +145 -0
  38. package/src/runtime/numbering-prefix.ts +47 -26
  39. package/src/runtime/page-layout-estimation.ts +212 -0
  40. package/src/runtime/read-only-diagnostics-runtime.ts +1 -0
  41. package/src/runtime/session-capabilities.ts +2 -0
  42. package/src/runtime/story-context.ts +164 -0
  43. package/src/runtime/story-targeting.ts +162 -0
  44. package/src/runtime/surface-projection.ts +239 -12
  45. package/src/runtime/table-schema.ts +87 -5
  46. package/src/runtime/view-state.ts +459 -0
  47. package/src/ui/WordReviewEditor.tsx +1902 -312
  48. package/src/ui/browser-export.ts +52 -0
  49. package/src/ui/headless/preserve-editor-selection.ts +5 -0
  50. package/src/ui/headless/selection-helpers.ts +20 -0
  51. package/src/ui/headless/selection-toolbar-model.ts +22 -0
  52. package/src/ui/headless/use-editor-keyboard.ts +6 -1
  53. package/src/ui-tailwind/chrome/tw-page-ruler.tsx +386 -0
  54. package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +125 -14
  55. package/src/ui-tailwind/editor-surface/perf-probe.ts +107 -0
  56. package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +45 -6
  57. package/src/ui-tailwind/editor-surface/pm-contextual-ui.ts +31 -0
  58. package/src/ui-tailwind/editor-surface/pm-position-map.ts +2 -2
  59. package/src/ui-tailwind/editor-surface/pm-schema.ts +47 -5
  60. package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +95 -22
  61. package/src/ui-tailwind/editor-surface/search-plugin.ts +19 -68
  62. package/src/ui-tailwind/editor-surface/tw-inline-token.tsx +11 -0
  63. package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +394 -77
  64. package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +0 -1
  65. package/src/ui-tailwind/index.ts +2 -1
  66. package/src/ui-tailwind/review/tw-comment-sidebar.tsx +277 -147
  67. package/src/ui-tailwind/review/tw-review-rail.tsx +6 -6
  68. package/src/ui-tailwind/theme/editor-theme.css +123 -0
  69. package/src/ui-tailwind/toolbar/tw-toolbar-icon-button.tsx +4 -0
  70. package/src/ui-tailwind/toolbar/tw-toolbar.tsx +291 -12
  71. package/src/ui-tailwind/tw-review-workspace.tsx +926 -27
  72. package/src/validation/compatibility-engine.ts +92 -20
  73. package/src/validation/diagnostics.ts +1 -0
  74. 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(document.content);
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
- blocks.push(...createSecondaryStoryPreviewBlocks(document, cursor));
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({ cells });
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
- numberingPrefix:
412
- numberingPrefixResolver.resolve(paragraph.numbering) ?? undefined,
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: "text",
629
+ kind: "note_ref",
573
630
  from: start,
574
631
  to: start + 1,
575
- text: node.noteId ?? "*",
576
- marks: ["superscript" as SurfaceTextMark],
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
- ): { label: string; detail: string } | null {
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
- return ["table", { class: "border-collapse w-full my-2 text-sm" }, ["tbody", 0]];
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
- return ["tr", 0];
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