@beyondwork/docx-react-component 1.0.2 → 1.0.4

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 CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  [![CI](https://github.com/bwllaming/React-OOXML-Office/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/bwllaming/React-OOXML-Office/actions/workflows/ci.yml)
4
4
 
5
- `docx-react-component` is now configured as an npm-ready package for the shipped product in this repository: `WordReviewEditor`, a fidelity-first React editor for legal-review-safe `docx` workflows.
5
+ `@beyondwork/docx-react-component` is now configured as an npm-ready package for the shipped product in this repository: `WordReviewEditor`, a fidelity-first React editor for legal-review-safe `docx` workflows.
6
6
 
7
7
  The broader repository is still evolving toward a layered `react-ooxml-office` platform, but the source reality is unchanged:
8
8
 
@@ -13,7 +13,7 @@ The broader repository is still evolving toward a layered `react-ooxml-office` p
13
13
  ## Install
14
14
 
15
15
  ```bash
16
- pnpm add docx-react-component react react-dom \
16
+ pnpm add @beyondwork/docx-react-component react react-dom tailwindcss \
17
17
  prosemirror-commands prosemirror-keymap prosemirror-model \
18
18
  prosemirror-state prosemirror-tables prosemirror-transform prosemirror-view
19
19
  ```
@@ -23,6 +23,7 @@ Current packaging truth:
23
23
  - the package is ESM-only
24
24
  - exports point at shipped TypeScript source entry points
25
25
  - consumers need a bundler or runtime that can resolve `.ts` and `.tsx` ESM imports
26
+ - consumers should import `@beyondwork/docx-react-component/ui-tailwind/theme/editor-theme.css` once and provide a Tailwind v4 CSS pipeline for it
26
27
  - package and source identifiers remain docx-first until a deliberate rename lands
27
28
 
28
29
  ## Shipped Product
@@ -30,7 +31,7 @@ Current packaging truth:
30
31
  The primary shipped surface is:
31
32
 
32
33
  ```tsx
33
- import { WordReviewEditor } from "docx-react-component";
34
+ import { WordReviewEditor } from "@beyondwork/docx-react-component";
34
35
 
35
36
  <WordReviewEditor />
36
37
  ```
@@ -43,9 +44,10 @@ import { WordReviewEditor } from "docx-react-component";
43
44
 
44
45
  The current public ESM exports are:
45
46
 
46
- - `docx-react-component` -> `WordReviewEditor`
47
- - `docx-react-component/public-types` -> public TypeScript contracts
48
- - `docx-react-component/ui-tailwind` -> current Tailwind UI primitives
47
+ - `@beyondwork/docx-react-component` -> `WordReviewEditor`
48
+ - `@beyondwork/docx-react-component/public-types` -> public TypeScript contracts
49
+ - `@beyondwork/docx-react-component/ui-tailwind` -> current Tailwind UI primitives
50
+ - `@beyondwork/docx-react-component/ui-tailwind/theme/editor-theme.css` -> shipped theme variables and Tailwind theme import
49
51
 
50
52
  ## Product Contract
51
53
 
@@ -84,6 +86,7 @@ Start here:
84
86
  Current shipped docx contracts:
85
87
 
86
88
  - `docs/reference/public-api.md`
89
+ This doc separates the shipped Wave 21 surface from the future Waves 25 through 27 ref expansion.
87
90
  - `docs/reference/ooxml-compliance.md`
88
91
  - `docs/reference/word-review-editor-frontend-architecture.md`
89
92
  - `docs/reference/word-review-editor-ux-guide.md`
@@ -99,9 +102,10 @@ Shared platform and planned xlsx docs:
99
102
 
100
103
  ## Packaging And Release
101
104
 
102
- - `.github/workflows/publish.yml` publishes on `v*` tags
105
+ - `.github/workflows/publish.yml` publishes on `v*` tags after verifying the tag matches `package.json`
103
106
  - `pnpm pack --dry-run` is the baseline package proof for this wave
104
107
  - npm provenance is enabled in `publishConfig`
108
+ - the published package currently ships source ESM entry points plus TypeScript source-backed `types` exports
105
109
  - the Microsoft Open XML SDK remains CI/internal-service only, never part of the shipped browser runtime
106
110
 
107
111
  ## Contribution Rules
package/package.json CHANGED
@@ -1,9 +1,8 @@
1
1
  {
2
2
  "name": "@beyondwork/docx-react-component",
3
3
  "publisher": "beyondwork",
4
- "version": "1.0.2",
4
+ "version": "1.0.4",
5
5
  "description": "Embeddable React Word (docx) editor with review, comments, tracked changes, and round-trip OOXML fidelity.",
6
- "packageManager": "pnpm@10.30.3",
7
6
  "type": "module",
8
7
  "files": [
9
8
  "README.md",
@@ -30,26 +29,10 @@
30
29
  "./io/docx-session": "./src/io/docx-session.ts",
31
30
  "./runtime/document-runtime": "./src/runtime/document-runtime.ts",
32
31
  "./api/public-types": "./src/api/public-types.ts",
32
+ "./ui-tailwind/theme/editor-theme.css": "./src/ui-tailwind/theme/editor-theme.css",
33
33
  "./package.json": "./package.json"
34
34
  },
35
35
  "types": "./src/index.ts",
36
- "scripts": {
37
- "build": "tsup",
38
- "test": "bash scripts/run-workspace-tests.sh",
39
- "test:repo": "pnpm exec tsx --test $(find test -type f \\( -name '*.test.ts' -o -name '*.test.tsx' \\) | sort)",
40
- "test:harness": "pnpm --filter @docx-react-component/react-word-editor-harness test",
41
- "lint:no-authored-js": "bash scripts/check-no-authored-js.sh",
42
- "context7:api-check": "bash scripts/context7-export-env.sh run bash scripts/context7-api-check.sh",
43
- "wave:doctor": "bash scripts/context7-export-env.sh run pnpm exec wave doctor --json",
44
- "wave:dry-run": "bash scripts/context7-export-env.sh run pnpm exec wave launch --lane main --dry-run --no-dashboard",
45
- "wave:launch": "bash scripts/context7-export-env.sh run pnpm exec wave launch --lane main",
46
- "wave:launch:managed": "bash scripts/wave-launch.sh",
47
- "wave:status": "bash scripts/wave-status.sh",
48
- "wave:watch": "bash scripts/wave-watch.sh --follow",
49
- "wave:dashboard:current": "bash scripts/wave-dashboard-attach.sh current",
50
- "wave:dashboard:global": "bash scripts/wave-dashboard-attach.sh global",
51
- "harness:dev": "pnpm --filter @docx-react-component/react-word-editor-harness dev"
52
- },
53
36
  "keywords": [
54
37
  "docx",
55
38
  "word",
@@ -71,6 +54,7 @@
71
54
  "@radix-ui/react-toggle": "^1.1.10",
72
55
  "@radix-ui/react-toggle-group": "^1.1.11",
73
56
  "@radix-ui/react-tooltip": "^1.2.8",
57
+ "fflate": "^0.8.2",
74
58
  "lucide-react": "^1.7.0"
75
59
  },
76
60
  "peerDependencies": {
@@ -82,7 +66,8 @@
82
66
  "prosemirror-transform": "^1.11.0",
83
67
  "prosemirror-view": "^1.41.7",
84
68
  "react": "^19.2.0",
85
- "react-dom": "^19.2.0"
69
+ "react-dom": "^19.2.0",
70
+ "tailwindcss": "^4.2.2"
86
71
  },
87
72
  "devDependencies": {
88
73
  "@chllming/wave-orchestration": "^0.9.8",
@@ -101,14 +86,21 @@
101
86
  "tsup": "^8.3.0",
102
87
  "tsx": "^4.21.0"
103
88
  },
104
- "pnpm": {
105
- "onlyBuiltDependencies": [
106
- "esbuild",
107
- "sharp"
108
- ],
109
- "overrides": {
110
- "react": "19.2.4",
111
- "react-dom": "19.2.4"
112
- }
89
+ "scripts": {
90
+ "build": "tsup",
91
+ "test": "bash scripts/run-workspace-tests.sh",
92
+ "test:repo": "pnpm exec tsx --test $(find test -type f \\( -name '*.test.ts' -o -name '*.test.tsx' \\) | sort)",
93
+ "test:harness": "pnpm --filter @docx-react-component/react-word-editor-harness test",
94
+ "lint:no-authored-js": "bash scripts/check-no-authored-js.sh",
95
+ "context7:api-check": "bash scripts/context7-export-env.sh run bash scripts/context7-api-check.sh",
96
+ "wave:doctor": "bash scripts/context7-export-env.sh run pnpm exec wave doctor --json",
97
+ "wave:dry-run": "bash scripts/context7-export-env.sh run pnpm exec wave launch --lane main --dry-run --no-dashboard",
98
+ "wave:launch": "bash scripts/context7-export-env.sh run pnpm exec wave launch --lane main",
99
+ "wave:launch:managed": "bash scripts/wave-launch.sh",
100
+ "wave:status": "bash scripts/wave-status.sh",
101
+ "wave:watch": "bash scripts/wave-watch.sh --follow",
102
+ "wave:dashboard:current": "bash scripts/wave-dashboard-attach.sh current",
103
+ "wave:dashboard:global": "bash scripts/wave-dashboard-attach.sh global",
104
+ "harness:dev": "pnpm --filter @docx-react-component/react-word-editor-harness dev"
113
105
  }
114
106
  }
@@ -1,5 +1,3 @@
1
- import { TextEncoder } from "node:util";
2
-
3
1
  import type { OpcRelationship } from "../io/ooxml/part-manifest.ts";
4
2
  import { serializeMainDocument } from "../io/export/serialize-main-document.ts";
5
3
  import {
@@ -9,8 +9,13 @@ import {
9
9
  } from "../state/editor-state.ts";
10
10
  import {
11
11
  areAnchorsEqual,
12
+ createDetachedAnchor,
12
13
  createEmptyMapping,
14
+ createNodeAnchor,
15
+ createRangeAnchor,
16
+ DEFAULT_BOUNDARY_ASSOC,
13
17
  mapAnchor,
18
+ type BoundaryAssoc,
14
19
  type EditorAnchorProjection,
15
20
  type MappingStep,
16
21
  type TransactionMapping,
@@ -655,20 +660,123 @@ function createTransaction(
655
660
  }
656
661
 
657
662
  function normalizeSelection(selection: SelectionSnapshot): SelectionSnapshot {
658
- if (selection.activeRange.kind === "range") {
663
+ const activeRange = normalizeSelectionAnchor(
664
+ selection.activeRange,
665
+ selection.anchor,
666
+ selection.head,
667
+ );
668
+
669
+ if (activeRange.kind === "range") {
659
670
  return {
660
- ...selection,
661
- anchor: selection.activeRange.range.from,
662
- head: selection.activeRange.range.to,
663
- isCollapsed: selection.activeRange.range.from === selection.activeRange.range.to,
671
+ anchor: activeRange.range.from,
672
+ head: activeRange.range.to,
673
+ isCollapsed: activeRange.range.from === activeRange.range.to,
674
+ activeRange,
664
675
  };
665
676
  }
666
677
 
667
- if (selection.activeRange.kind === "node") {
668
- return createSelectionSnapshot(selection.activeRange.at, selection.activeRange.at);
678
+ if (activeRange.kind === "node") {
679
+ return createSelectionSnapshot(activeRange.at, activeRange.at);
680
+ }
681
+
682
+ return {
683
+ anchor: activeRange.lastKnownRange.from,
684
+ head: activeRange.lastKnownRange.to,
685
+ isCollapsed: activeRange.lastKnownRange.from === activeRange.lastKnownRange.to,
686
+ activeRange,
687
+ };
688
+ }
689
+
690
+ function normalizeSelectionAnchor(
691
+ value: SelectionSnapshot["activeRange"],
692
+ anchor: number,
693
+ head: number,
694
+ ): EditorAnchorProjection {
695
+ if (!value || typeof value !== "object") {
696
+ return createRangeAnchor(anchor, head, DEFAULT_BOUNDARY_ASSOC);
697
+ }
698
+
699
+ const record = value as unknown as Record<string, unknown>;
700
+ switch (record.kind) {
701
+ case "range": {
702
+ const assoc = normalizeBoundaryAssoc(record.assoc);
703
+ const internalRange = record.range;
704
+ if (
705
+ internalRange &&
706
+ typeof internalRange === "object" &&
707
+ typeof (internalRange as { from?: unknown }).from === "number" &&
708
+ typeof (internalRange as { to?: unknown }).to === "number"
709
+ ) {
710
+ return createRangeAnchor(
711
+ (internalRange as { from: number }).from,
712
+ (internalRange as { to: number }).to,
713
+ assoc,
714
+ );
715
+ }
716
+
717
+ if (typeof record.from === "number" && typeof record.to === "number") {
718
+ return createRangeAnchor(record.from, record.to, assoc);
719
+ }
720
+
721
+ return createRangeAnchor(anchor, head, assoc);
722
+ }
723
+ case "node":
724
+ return createNodeAnchor(
725
+ typeof record.at === "number" ? record.at : anchor,
726
+ normalizeAssoc(record.assoc),
727
+ );
728
+ case "detached": {
729
+ const lastKnownRange = record.lastKnownRange;
730
+ if (
731
+ lastKnownRange &&
732
+ typeof lastKnownRange === "object" &&
733
+ typeof (lastKnownRange as { from?: unknown }).from === "number" &&
734
+ typeof (lastKnownRange as { to?: unknown }).to === "number"
735
+ ) {
736
+ return createDetachedAnchor(
737
+ {
738
+ from: (lastKnownRange as { from: number }).from,
739
+ to: (lastKnownRange as { to: number }).to,
740
+ },
741
+ normalizeDetachedReason(record.reason),
742
+ );
743
+ }
744
+
745
+ return createDetachedAnchor({ from: anchor, to: head }, normalizeDetachedReason(record.reason));
746
+ }
747
+ default:
748
+ return createRangeAnchor(anchor, head, DEFAULT_BOUNDARY_ASSOC);
749
+ }
750
+ }
751
+
752
+ function normalizeBoundaryAssoc(value: unknown): BoundaryAssoc {
753
+ if (value && typeof value === "object") {
754
+ const record = value as unknown as Record<string, unknown>;
755
+ return {
756
+ start: normalizeAssoc(record.start),
757
+ end: normalizeAssoc(record.end),
758
+ };
759
+ }
760
+
761
+ return DEFAULT_BOUNDARY_ASSOC;
762
+ }
763
+
764
+ function normalizeAssoc(value: unknown): -1 | 1 {
765
+ return value === -1 ? -1 : 1;
766
+ }
767
+
768
+ function normalizeDetachedReason(
769
+ value: unknown,
770
+ ): "deleted" | "invalidatedByStructureChange" | "importAmbiguity" {
771
+ if (
772
+ value === "deleted" ||
773
+ value === "invalidatedByStructureChange" ||
774
+ value === "importAmbiguity"
775
+ ) {
776
+ return value;
669
777
  }
670
778
 
671
- return selection;
779
+ return "importAmbiguity";
672
780
  }
673
781
 
674
782
  function applyTextCommand(
@@ -10,6 +10,8 @@ import {
10
10
  type StoryUnit,
11
11
  type TextStory,
12
12
  } from "../schema/text-schema.ts";
13
+ import { createEditorSurfaceSnapshot } from "../../runtime/surface-projection.ts";
14
+ import type { DocumentRootNode, ParagraphNode, TableNode } from "../../model/canonical-document.ts";
13
15
 
14
16
  export type TextInsertion =
15
17
  | {
@@ -69,6 +71,27 @@ export function applyTextTransaction(
69
71
  options: {
70
72
  timestamp: string;
71
73
  },
74
+ ): TextTransactionResult {
75
+ const tableScopedResult = tryApplyTableParagraphTransaction(
76
+ document,
77
+ selection,
78
+ intent,
79
+ options,
80
+ );
81
+ if (tableScopedResult) {
82
+ return tableScopedResult;
83
+ }
84
+
85
+ return applyLinearTextTransaction(document, selection, intent, options);
86
+ }
87
+
88
+ function applyLinearTextTransaction(
89
+ document: CanonicalDocumentEnvelope,
90
+ selection: SelectionSnapshot,
91
+ intent: TextTransactionIntent,
92
+ options: {
93
+ timestamp: string;
94
+ },
72
95
  ): TextTransactionResult {
73
96
  const story = parseTextStory(document.content);
74
97
  const normalizedRange = resolveRange(selection, story.size, intent);
@@ -118,6 +141,209 @@ export function applyTextTransaction(
118
141
  };
119
142
  }
120
143
 
144
+ function tryApplyTableParagraphTransaction(
145
+ document: CanonicalDocumentEnvelope,
146
+ selection: SelectionSnapshot,
147
+ intent: TextTransactionIntent,
148
+ options: {
149
+ timestamp: string;
150
+ },
151
+ ): TextTransactionResult | null {
152
+ const scope = resolveTableParagraphScope(document, selection);
153
+ if (!scope) {
154
+ return null;
155
+ }
156
+ if (scope === "unsupported") {
157
+ throw new TextTransactionError(
158
+ "unsupported_content",
159
+ "Text transactions inside table structures are only supported when the selection stays within one paragraph-backed table cell.",
160
+ );
161
+ }
162
+
163
+ const localDocument: CanonicalDocumentEnvelope = {
164
+ ...document,
165
+ content: {
166
+ type: "doc",
167
+ children: [scope.paragraph],
168
+ },
169
+ };
170
+ const localSelection = createSelectionSnapshot(
171
+ selection.anchor - scope.paragraphStart,
172
+ selection.head - scope.paragraphStart,
173
+ );
174
+ const localResult = applyLinearTextTransaction(localDocument, localSelection, intent, options);
175
+ const nextParagraphBlocks = (localResult.document.content as DocumentRootNode).children;
176
+ const nextRoot: DocumentRootNode = {
177
+ ...scope.root,
178
+ children: scope.root.children.map((child, blockIndex) => {
179
+ if (blockIndex !== scope.tableBlockIndex) {
180
+ return child;
181
+ }
182
+
183
+ const table = child as TableNode;
184
+ return {
185
+ ...table,
186
+ rows: table.rows.map((row, rowIndex) => {
187
+ if (rowIndex !== scope.rowIndex) {
188
+ return row;
189
+ }
190
+
191
+ return {
192
+ ...row,
193
+ cells: row.cells.map((cell, cellIndex) => {
194
+ if (cellIndex !== scope.cellIndex) {
195
+ return cell;
196
+ }
197
+
198
+ return {
199
+ ...cell,
200
+ children: [
201
+ ...cell.children.slice(0, scope.childIndex),
202
+ ...nextParagraphBlocks,
203
+ ...cell.children.slice(scope.childIndex + 1),
204
+ ],
205
+ };
206
+ }),
207
+ };
208
+ }),
209
+ };
210
+ }),
211
+ };
212
+
213
+ return {
214
+ document: {
215
+ ...document,
216
+ updatedAt: options.timestamp,
217
+ content: nextRoot,
218
+ },
219
+ selection: createSelectionSnapshot(
220
+ localResult.selection.anchor + scope.paragraphStart,
221
+ localResult.selection.head + scope.paragraphStart,
222
+ ),
223
+ mapping: {
224
+ ...localResult.mapping,
225
+ steps: localResult.mapping.steps.map((step) => ({
226
+ ...step,
227
+ from: step.from + scope.paragraphStart,
228
+ to: step.to + scope.paragraphStart,
229
+ })),
230
+ },
231
+ storyText: localResult.storyText,
232
+ };
233
+ }
234
+
235
+ function resolveTableParagraphScope(
236
+ document: CanonicalDocumentEnvelope,
237
+ selection: SelectionSnapshot,
238
+ ):
239
+ | {
240
+ root: DocumentRootNode;
241
+ tableBlockIndex: number;
242
+ rowIndex: number;
243
+ cellIndex: number;
244
+ childIndex: number;
245
+ paragraph: ParagraphNode;
246
+ paragraphStart: number;
247
+ }
248
+ | "unsupported"
249
+ | null {
250
+ const root = document.content as DocumentRootNode;
251
+ if (!root || root.type !== "doc" || !Array.isArray(root.children)) {
252
+ return null;
253
+ }
254
+
255
+ const surface = createEditorSurfaceSnapshot(document, selection);
256
+ const selectionFrom = Math.min(selection.anchor, selection.head);
257
+ const selectionTo = Math.max(selection.anchor, selection.head);
258
+
259
+ for (let blockIndex = 0; blockIndex < root.children.length; blockIndex += 1) {
260
+ const block = root.children[blockIndex];
261
+ const surfaceBlock = surface.blocks[blockIndex];
262
+ if (block?.type !== "table" || surfaceBlock?.kind !== "table") {
263
+ continue;
264
+ }
265
+
266
+ const insideTable = selectionFallsWithinSurfaceRange(
267
+ selectionFrom,
268
+ selectionTo,
269
+ surfaceBlock.from,
270
+ surfaceBlock.to,
271
+ );
272
+ if (!insideTable) {
273
+ continue;
274
+ }
275
+
276
+ for (let rowIndex = 0; rowIndex < block.rows.length; rowIndex += 1) {
277
+ const row = block.rows[rowIndex];
278
+ const surfaceRow = surfaceBlock.rows[rowIndex];
279
+ if (!row || !surfaceRow) {
280
+ continue;
281
+ }
282
+
283
+ for (let cellIndex = 0; cellIndex < row.cells.length; cellIndex += 1) {
284
+ const cell = row.cells[cellIndex];
285
+ const surfaceCell = surfaceRow.cells[cellIndex];
286
+ if (!cell || !surfaceCell) {
287
+ continue;
288
+ }
289
+
290
+ for (
291
+ let childIndex = 0;
292
+ childIndex < Math.min(cell.children.length, surfaceCell.content.length);
293
+ childIndex += 1
294
+ ) {
295
+ const child = cell.children[childIndex];
296
+ const surfaceChild = surfaceCell.content[childIndex];
297
+ if (child?.type !== "paragraph" || surfaceChild?.kind !== "paragraph") {
298
+ continue;
299
+ }
300
+
301
+ const insideParagraph = selectionFallsWithinSurfaceRange(
302
+ selectionFrom,
303
+ selectionTo,
304
+ surfaceChild.from,
305
+ surfaceChild.to,
306
+ );
307
+ if (!insideParagraph) {
308
+ continue;
309
+ }
310
+
311
+ return {
312
+ root,
313
+ tableBlockIndex: blockIndex,
314
+ rowIndex,
315
+ cellIndex,
316
+ childIndex,
317
+ paragraph: child,
318
+ paragraphStart: surfaceChild.from,
319
+ };
320
+ }
321
+ }
322
+ }
323
+
324
+ return "unsupported";
325
+ }
326
+
327
+ return null;
328
+ }
329
+
330
+ function selectionFallsWithinSurfaceRange(
331
+ selectionFrom: number,
332
+ selectionTo: number,
333
+ rangeFrom: number,
334
+ rangeTo: number,
335
+ ): boolean {
336
+ if (rangeFrom === rangeTo) {
337
+ return selectionFrom === rangeFrom && selectionTo === rangeTo;
338
+ }
339
+
340
+ return (
341
+ selectionFrom >= rangeFrom &&
342
+ selectionTo <= rangeTo &&
343
+ selectionFrom < rangeTo
344
+ );
345
+ }
346
+
121
347
  function resolveRange(
122
348
  selection: SelectionSnapshot,
123
349
  storySize: number,
@@ -302,5 +302,5 @@ function convertCachedFormulaValue(
302
302
  // ---------------------------------------------------------------------------
303
303
 
304
304
  function decodePartBytes(bytes: Uint8Array): string {
305
- return Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength).toString("utf8");
305
+ return new TextDecoder("utf-8").decode(bytes);
306
306
  }
@@ -1733,7 +1733,7 @@ function decodeUtf8(bytes: Uint8Array | undefined): string {
1733
1733
  return "";
1734
1734
  }
1735
1735
 
1736
- return Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength).toString("utf8");
1736
+ return new TextDecoder("utf-8").decode(bytes);
1737
1737
  }
1738
1738
 
1739
1739
  function toUint8Array(bytes: Uint8Array | ArrayBuffer): Uint8Array {
@@ -6,6 +6,7 @@ import type {
6
6
  ParsedTableBorders,
7
7
  ParsedTableCell,
8
8
  ParsedTableCellBorders,
9
+ ParsedTableLook,
9
10
  ParsedTableRow,
10
11
  ParsedTableWidth,
11
12
  } from "../ooxml/parse-tables.ts";
@@ -36,6 +37,9 @@ function serializeCell(cell: ParsedTableCell): string {
36
37
 
37
38
  function buildTablePropertiesXml(table: ParsedTable): string {
38
39
  const children: string[] = [];
40
+ if (table.styleId) {
41
+ children.push(`<w:tblStyle w:val="${table.styleId}"/>`);
42
+ }
39
43
  if (table.width) {
40
44
  children.push(serializeWidth("tblW", table.width));
41
45
  }
@@ -50,6 +54,10 @@ function buildTablePropertiesXml(table: ParsedTable): string {
50
54
  const marginsXml = serializeTableCellMargins(table.cellMargins);
51
55
  if (marginsXml) children.push(`<w:tblCellMar>${marginsXml}</w:tblCellMar>`);
52
56
  }
57
+ if (table.tblLook) {
58
+ const tblLookXml = serializeTableLook(table.tblLook);
59
+ if (tblLookXml) children.push(tblLookXml);
60
+ }
53
61
  return children.length > 0 ? `<w:tblPr>${children.join("")}</w:tblPr>` : "";
54
62
  }
55
63
 
@@ -128,6 +136,25 @@ function serializeCellBorders(borders: ParsedTableCellBorders): string {
128
136
  .join("");
129
137
  }
130
138
 
139
+ function serializeTableLook(tblLook: ParsedTableLook): string {
140
+ const attrs: string[] = [];
141
+ if (tblLook.val) attrs.push(`w:val="${tblLook.val}"`);
142
+ for (const [key, attr] of [
143
+ ["firstRow", "w:firstRow"],
144
+ ["lastRow", "w:lastRow"],
145
+ ["firstColumn", "w:firstColumn"],
146
+ ["lastColumn", "w:lastColumn"],
147
+ ["noHBand", "w:noHBand"],
148
+ ["noVBand", "w:noVBand"],
149
+ ] as const) {
150
+ const value = tblLook[key];
151
+ if (value !== undefined) {
152
+ attrs.push(`${attr}="${value ? "1" : "0"}"`);
153
+ }
154
+ }
155
+ return attrs.length > 0 ? `<w:tblLook ${attrs.join(" ")}/>` : "";
156
+ }
157
+
131
158
  function serializeCellShading(shading: ParsedCellShading): string {
132
159
  const attrs: string[] = [];
133
160
  if (shading.val) attrs.push(`w:val="${shading.val}"`);