@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.
@@ -59,12 +59,23 @@ export interface ParsedCellMargins {
59
59
  right?: number;
60
60
  }
61
61
 
62
+ export interface ParsedTableLook {
63
+ val?: string;
64
+ firstRow?: boolean;
65
+ lastRow?: boolean;
66
+ firstColumn?: boolean;
67
+ lastColumn?: boolean;
68
+ noHBand?: boolean;
69
+ noVBand?: boolean;
70
+ }
71
+
62
72
  export interface ParsedTableDocument {
63
73
  tables: ParsedTable[];
64
74
  }
65
75
 
66
76
  export interface ParsedTable {
67
77
  type: "table";
78
+ styleId?: string;
68
79
  propertiesXml?: string;
69
80
  gridColumns: number[];
70
81
  rows: ParsedTableRow[];
@@ -73,6 +84,7 @@ export interface ParsedTable {
73
84
  alignment?: "left" | "center" | "right";
74
85
  borders?: ParsedTableBorders;
75
86
  cellMargins?: ParsedCellMargins;
87
+ tblLook?: ParsedTableLook;
76
88
  }
77
89
 
78
90
  export interface ParsedTableRow {
@@ -115,13 +127,16 @@ function parseTable(node: XmlElementNode, sourceXml: string): ParsedTable {
115
127
  .filter((child): child is XmlElementNode => child.type === "element" && localName(child.name) === "tr")
116
128
  .map((rowNode) => parseRow(rowNode, sourceXml));
117
129
 
130
+ const styleId = propertiesNode ? readTableStyleId(propertiesNode) : undefined;
118
131
  const width = propertiesNode ? readTableWidth(propertiesNode) : undefined;
119
132
  const alignment = propertiesNode ? readTableAlignment(propertiesNode) : undefined;
120
133
  const borders = propertiesNode ? readTableBorders(propertiesNode) : undefined;
121
134
  const cellMargins = propertiesNode ? readTableCellMargins(propertiesNode) : undefined;
135
+ const tblLook = propertiesNode ? readTableLook(propertiesNode) : undefined;
122
136
 
123
137
  return {
124
138
  type: "table",
139
+ ...(styleId ? { styleId } : {}),
125
140
  ...(propertiesNode ? { propertiesXml: sourceXml.slice(propertiesNode.start, propertiesNode.end) } : {}),
126
141
  gridColumns: gridNode ? readGridColumns(gridNode) : [],
127
142
  rows,
@@ -130,6 +145,7 @@ function parseTable(node: XmlElementNode, sourceXml: string): ParsedTable {
130
145
  ...(alignment ? { alignment } : {}),
131
146
  ...(borders ? { borders } : {}),
132
147
  ...(cellMargins ? { cellMargins } : {}),
148
+ ...(tblLook ? { tblLook } : {}),
133
149
  };
134
150
  }
135
151
 
@@ -493,6 +509,40 @@ function readTableAlignment(propertiesNode: XmlElementNode): "left" | "center" |
493
509
  return undefined;
494
510
  }
495
511
 
512
+ function readTableStyleId(propertiesNode: XmlElementNode): string | undefined {
513
+ const styleNode = findFirstChild(propertiesNode, "tblStyle");
514
+ if (!styleNode) return undefined;
515
+ return styleNode.attributes["w:val"] ?? styleNode.attributes.val;
516
+ }
517
+
518
+ function readTableLook(propertiesNode: XmlElementNode): ParsedTableLook | undefined {
519
+ const tblLookNode = findFirstChild(propertiesNode, "tblLook");
520
+ if (!tblLookNode) return undefined;
521
+
522
+ const tableLook: ParsedTableLook = {};
523
+ const val = tblLookNode.attributes["w:val"] ?? tblLookNode.attributes.val;
524
+ if (val) {
525
+ tableLook.val = val;
526
+ }
527
+
528
+ for (const [attribute, key] of [
529
+ ["w:firstRow", "firstRow"],
530
+ ["w:lastRow", "lastRow"],
531
+ ["w:firstColumn", "firstColumn"],
532
+ ["w:lastColumn", "lastColumn"],
533
+ ["w:noHBand", "noHBand"],
534
+ ["w:noVBand", "noVBand"],
535
+ ] as const) {
536
+ const fallback = attribute.replace("w:", "");
537
+ const raw = tblLookNode.attributes[attribute] ?? tblLookNode.attributes[fallback];
538
+ if (raw !== undefined) {
539
+ tableLook[key] = raw !== "0" && raw !== "false" && raw !== "off";
540
+ }
541
+ }
542
+
543
+ return Object.keys(tableLook).length > 0 ? tableLook : undefined;
544
+ }
545
+
496
546
  function readTableBorders(propertiesNode: XmlElementNode): ParsedTableBorders | undefined {
497
547
  const bordersNode = findFirstChild(propertiesNode, "tblBorders");
498
548
  if (!bordersNode) return undefined;
@@ -1,4 +1,4 @@
1
- import { inflateRawSync } from "node:zlib";
1
+ import { inflateSync } from "fflate";
2
2
 
3
3
  import {
4
4
  CONTENT_TYPES_PATH,
@@ -35,8 +35,7 @@ const LOCAL_FILE_HEADER_SIGNATURE = 0x04034b50;
35
35
 
36
36
  export function readOpcPackage(source: Uint8Array | ArrayBuffer): OpcPackage {
37
37
  const bytes = source instanceof Uint8Array ? source : new Uint8Array(source);
38
- const archive = Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength);
39
- const centralDirectory = readCentralDirectory(archive);
38
+ const centralDirectory = readCentralDirectory(bytes);
40
39
  const parts = new Map<string, OpcPackagePart>();
41
40
 
42
41
  for (const entry of centralDirectory) {
@@ -44,7 +43,7 @@ export function readOpcPackage(source: Uint8Array | ArrayBuffer): OpcPackage {
44
43
  continue;
45
44
  }
46
45
 
47
- const storedBytes = readZipEntry(archive, entry);
46
+ const storedBytes = readZipEntry(bytes, entry);
48
47
  const normalizedPath = normalizePartPath(entry.path);
49
48
  const surfaceKind = getSurfaceKind(normalizedPath);
50
49
 
@@ -112,30 +111,31 @@ export function readOpcPackage(source: Uint8Array | ArrayBuffer): OpcPackage {
112
111
  };
113
112
  }
114
113
 
115
- function readCentralDirectory(archive: Buffer): ZipCentralDirectoryEntry[] {
116
- const eocdOffset = findEndOfCentralDirectory(archive);
117
- const entryCount = archive.readUInt16LE(eocdOffset + 10);
118
- const centralDirectoryOffset = archive.readUInt32LE(eocdOffset + 16);
114
+ function readCentralDirectory(archive: Uint8Array): ZipCentralDirectoryEntry[] {
115
+ const view = new DataView(archive.buffer, archive.byteOffset, archive.byteLength);
116
+ const eocdOffset = findEndOfCentralDirectory(archive, view);
117
+ const entryCount = view.getUint16(eocdOffset + 10, true);
118
+ const centralDirectoryOffset = view.getUint32(eocdOffset + 16, true);
119
119
  const entries: ZipCentralDirectoryEntry[] = [];
120
120
  let offset = centralDirectoryOffset;
121
121
 
122
122
  for (let index = 0; index < entryCount; index += 1) {
123
- const signature = archive.readUInt32LE(offset);
123
+ const signature = view.getUint32(offset, true);
124
124
  if (signature !== CENTRAL_DIRECTORY_SIGNATURE) {
125
125
  throw new Error(`Invalid ZIP central directory signature at offset ${offset}.`);
126
126
  }
127
127
 
128
- const generalPurposeBitFlag = archive.readUInt16LE(offset + 8);
129
- const compressionMethod = archive.readUInt16LE(offset + 10);
130
- const crc32 = archive.readUInt32LE(offset + 16);
131
- const compressedSize = archive.readUInt32LE(offset + 20);
132
- const uncompressedSize = archive.readUInt32LE(offset + 24);
133
- const fileNameLength = archive.readUInt16LE(offset + 28);
134
- const extraFieldLength = archive.readUInt16LE(offset + 30);
135
- const fileCommentLength = archive.readUInt16LE(offset + 32);
136
- const localHeaderOffset = archive.readUInt32LE(offset + 42);
128
+ const generalPurposeBitFlag = view.getUint16(offset + 8, true);
129
+ const compressionMethod = view.getUint16(offset + 10, true);
130
+ const crc32 = view.getUint32(offset + 16, true);
131
+ const compressedSize = view.getUint32(offset + 20, true);
132
+ const uncompressedSize = view.getUint32(offset + 24, true);
133
+ const fileNameLength = view.getUint16(offset + 28, true);
134
+ const extraFieldLength = view.getUint16(offset + 30, true);
135
+ const fileCommentLength = view.getUint16(offset + 32, true);
136
+ const localHeaderOffset = view.getUint32(offset + 42, true);
137
137
  const fileNameOffset = offset + 46;
138
- const fileName = archive.subarray(fileNameOffset, fileNameOffset + fileNameLength).toString("utf8");
138
+ const fileName = decodeUtf8(archive.subarray(fileNameOffset, fileNameOffset + fileNameLength));
139
139
 
140
140
  entries.push({
141
141
  path: fileName,
@@ -153,13 +153,13 @@ function readCentralDirectory(archive: Buffer): ZipCentralDirectoryEntry[] {
153
153
  return entries;
154
154
  }
155
155
 
156
- function findEndOfCentralDirectory(archive: Buffer): number {
156
+ function findEndOfCentralDirectory(archive: Uint8Array, view: DataView): number {
157
157
  const minimumEocdSize = 22;
158
158
  const maximumCommentSize = 0xffff;
159
159
  const searchStart = Math.max(0, archive.length - minimumEocdSize - maximumCommentSize);
160
160
 
161
161
  for (let offset = archive.length - minimumEocdSize; offset >= searchStart; offset -= 1) {
162
- if (archive.readUInt32LE(offset) === EOCD_SIGNATURE) {
162
+ if (view.getUint32(offset, true) === EOCD_SIGNATURE) {
163
163
  return offset;
164
164
  }
165
165
  }
@@ -167,15 +167,16 @@ function findEndOfCentralDirectory(archive: Buffer): number {
167
167
  throw new Error("Invalid ZIP archive: end of central directory not found.");
168
168
  }
169
169
 
170
- function readZipEntry(archive: Buffer, entry: ZipCentralDirectoryEntry): Uint8Array {
170
+ function readZipEntry(archive: Uint8Array, entry: ZipCentralDirectoryEntry): Uint8Array {
171
+ const view = new DataView(archive.buffer, archive.byteOffset, archive.byteLength);
171
172
  const headerOffset = entry.localHeaderOffset;
172
- const signature = archive.readUInt32LE(headerOffset);
173
+ const signature = view.getUint32(headerOffset, true);
173
174
  if (signature !== LOCAL_FILE_HEADER_SIGNATURE) {
174
175
  throw new Error(`Invalid ZIP local file header signature at offset ${headerOffset}.`);
175
176
  }
176
177
 
177
- const fileNameLength = archive.readUInt16LE(headerOffset + 26);
178
- const extraFieldLength = archive.readUInt16LE(headerOffset + 28);
178
+ const fileNameLength = view.getUint16(headerOffset + 26, true);
179
+ const extraFieldLength = view.getUint16(headerOffset + 28, true);
179
180
  const dataOffset = headerOffset + 30 + fileNameLength + extraFieldLength;
180
181
  const compressed = archive.subarray(dataOffset, dataOffset + entry.compressedSize);
181
182
 
@@ -183,7 +184,7 @@ function readZipEntry(archive: Buffer, entry: ZipCentralDirectoryEntry): Uint8Ar
183
184
  case "store":
184
185
  return new Uint8Array(compressed);
185
186
  case "deflate":
186
- return new Uint8Array(inflateRawSync(compressed));
187
+ return inflateSync(compressed);
187
188
  default:
188
189
  throw new Error(`Unsupported ZIP compression for ${entry.path}.`);
189
190
  }
@@ -213,7 +214,11 @@ function getSurfaceKind(path: string): OpcPartManifestEntry["surfaceKind"] {
213
214
  }
214
215
 
215
216
  function decodeXml(bytes: Uint8Array): string {
216
- return Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength).toString("utf8");
217
+ return new TextDecoder("utf-8").decode(bytes);
218
+ }
219
+
220
+ function decodeUtf8(bytes: Uint8Array): string {
221
+ return new TextDecoder("utf-8").decode(bytes);
217
222
  }
218
223
 
219
224
  function parseContentTypesXml(xml: string): OpcPackageManifest["contentTypes"] {
@@ -11,6 +11,7 @@
11
11
 
12
12
  import type { Command as PMCommand, EditorState } from "prosemirror-state";
13
13
  import {
14
+ TableMap,
14
15
  addColumnAfter as pmAddColumnAfter,
15
16
  addColumnBefore as pmAddColumnBefore,
16
17
  addRowAfter as pmAddRowAfter,
@@ -48,8 +49,14 @@ function withTableGuard(
48
49
  command: PMCommand,
49
50
  ): PMCommand {
50
51
  return (state, dispatch, view) => {
51
- if (!canRun(state)) return false;
52
- return command(state, dispatch, view);
52
+ try {
53
+ if (!canRun(state)) {
54
+ return false;
55
+ }
56
+ return command(state, dispatch, view);
57
+ } catch {
58
+ return false;
59
+ }
53
60
  };
54
61
  }
55
62
 
@@ -72,7 +79,7 @@ export const addColumnAfter: PMCommand = (state, dispatch, view) =>
72
79
 
73
80
  export const deleteColumn: PMCommand = withTableGuard((state) => {
74
81
  const table = tableAtSelection(state);
75
- return table !== null && table.childCount > 0 && table.firstChild !== null && table.firstChild.childCount > 1;
82
+ return table !== null && TableMap.get(table).width > 1;
76
83
  }, pmDeleteColumn);
77
84
 
78
85
  export {
@@ -11,6 +11,14 @@
11
11
 
12
12
  import type { NodeSpec } from "prosemirror-model";
13
13
 
14
+ type TableCellAttrs = {
15
+ colspan?: number | null;
16
+ rowspan?: number | null;
17
+ colwidth?: number[] | null;
18
+ gridSpan?: number | null;
19
+ verticalMerge?: "restart" | "continue" | null;
20
+ };
21
+
14
22
  function resolveRenderedColspan(attrs: {
15
23
  colspan?: number | null;
16
24
  gridSpan?: number | null;
@@ -24,6 +32,93 @@ function resolveRenderedColspan(attrs: {
24
32
  return 1;
25
33
  }
26
34
 
35
+ function resolveRenderedRowspan(attrs: { rowspan?: number | null }): number {
36
+ return typeof attrs.rowspan === "number" && attrs.rowspan > 1 ? attrs.rowspan : 1;
37
+ }
38
+
39
+ function parseColwidthAttr(dom: HTMLElement, colspan: number): number[] | null {
40
+ const widthAttr = dom.getAttribute("data-colwidth");
41
+ if (!widthAttr || !/^\d+(,\d+)*$/.test(widthAttr)) {
42
+ return null;
43
+ }
44
+
45
+ const widths = widthAttr.split(",").map((value) => Number.parseInt(value, 10));
46
+ return widths.length === colspan ? widths : null;
47
+ }
48
+
49
+ function getCellAttrs(dom: HTMLElement): TableCellAttrs {
50
+ const colspan = Number(dom.getAttribute("colspan") || 1);
51
+ const rowspan = Number(dom.getAttribute("rowspan") || 1);
52
+ const gridSpanAttr = dom.getAttribute("data-grid-span");
53
+ const verticalMergeAttr = dom.getAttribute("data-vertical-merge");
54
+ const gridSpan = gridSpanAttr ? Number.parseInt(gridSpanAttr, 10) : colspan;
55
+
56
+ return {
57
+ colspan,
58
+ rowspan,
59
+ colwidth: parseColwidthAttr(dom, colspan),
60
+ gridSpan: Number.isFinite(gridSpan) && gridSpan > 0 ? gridSpan : colspan,
61
+ verticalMerge:
62
+ verticalMergeAttr === "restart" || verticalMergeAttr === "continue"
63
+ ? verticalMergeAttr
64
+ : null,
65
+ };
66
+ }
67
+
68
+ function setCellDomAttrs(nodeAttrs: TableCellAttrs, className: string): Record<string, string> {
69
+ const attrs: Record<string, string> = { class: className };
70
+ const colspan = resolveRenderedColspan(nodeAttrs);
71
+ const rowspan = resolveRenderedRowspan(nodeAttrs);
72
+
73
+ if (colspan > 1) {
74
+ attrs.colspan = String(colspan);
75
+ }
76
+ if (rowspan > 1) {
77
+ attrs.rowspan = String(rowspan);
78
+ }
79
+ if (nodeAttrs.colwidth && nodeAttrs.colwidth.length > 0) {
80
+ attrs["data-colwidth"] = nodeAttrs.colwidth.join(",");
81
+ }
82
+ if (typeof nodeAttrs.gridSpan === "number" && nodeAttrs.gridSpan > 1) {
83
+ attrs["data-grid-span"] = String(nodeAttrs.gridSpan);
84
+ }
85
+ if (nodeAttrs.verticalMerge) {
86
+ attrs["data-vertical-merge"] = nodeAttrs.verticalMerge;
87
+ }
88
+
89
+ return attrs;
90
+ }
91
+
92
+ function validateColwidth(value: unknown): void {
93
+ if (value === null) {
94
+ return;
95
+ }
96
+ if (!Array.isArray(value)) {
97
+ throw new TypeError("colwidth must be null or an array");
98
+ }
99
+ for (const item of value) {
100
+ if (typeof item !== "number") {
101
+ throw new TypeError("colwidth must be null or an array of numbers");
102
+ }
103
+ }
104
+ }
105
+
106
+ function validateVerticalMerge(value: unknown): void {
107
+ if (value === null || value === "restart" || value === "continue") {
108
+ return;
109
+ }
110
+ throw new TypeError("verticalMerge must be null, 'restart', or 'continue'");
111
+ }
112
+
113
+ const tableCellSpecAttrs = {
114
+ propertiesXml: { default: null },
115
+ gridSpan: { default: 1, validate: "number" },
116
+ verticalMerge: { default: null, validate: validateVerticalMerge },
117
+ colspan: { default: 1, validate: "number" },
118
+ rowspan: { default: 1, validate: "number" },
119
+ colwidth: { default: null, validate: validateColwidth },
120
+ } as const;
121
+
27
122
  export const tableNodeSpec: NodeSpec = {
28
123
  content: "table_row+",
29
124
  tableRole: "table",
@@ -56,35 +151,24 @@ export const tableCellNodeSpec: NodeSpec = {
56
151
  content: "paragraph+",
57
152
  tableRole: "cell",
58
153
  isolating: true,
59
- attrs: {
60
- propertiesXml: { default: null },
61
- gridSpan: { default: 1 },
62
- verticalMerge: { default: null },
63
- colspan: { default: 1 },
64
- rowspan: { default: 1 },
65
- colwidth: { default: null },
66
- },
154
+ attrs: tableCellSpecAttrs,
67
155
  parseDOM: [
68
156
  {
69
157
  tag: "td",
70
- getAttrs(dom: HTMLElement) {
71
- const colspan = dom.getAttribute("colspan");
72
- const rowspan = dom.getAttribute("rowspan");
73
- return {
74
- colspan: colspan ? Number.parseInt(colspan, 10) : 1,
75
- rowspan: rowspan ? Number.parseInt(rowspan, 10) : 1,
76
- };
158
+ getAttrs(dom) {
159
+ if (!(dom instanceof HTMLElement)) {
160
+ return false;
161
+ }
162
+ return getCellAttrs(dom);
77
163
  },
78
164
  },
79
165
  ],
80
166
  toDOM(node) {
81
- const attrs: Record<string, string> = {
82
- class: "border border-primary/20 p-2 align-top",
83
- };
84
- const colspan = resolveRenderedColspan(node.attrs as { colspan?: number; gridSpan?: number });
85
- if (colspan > 1) attrs.colspan = String(colspan);
86
- if (node.attrs.rowspan > 1) attrs.rowspan = String(node.attrs.rowspan);
87
- return ["td", attrs, 0];
167
+ return [
168
+ "td",
169
+ setCellDomAttrs(node.attrs as TableCellAttrs, "border border-primary/20 p-2 align-top"),
170
+ 0,
171
+ ];
88
172
  },
89
173
  };
90
174
 
@@ -92,35 +176,27 @@ export const tableHeaderCellNodeSpec: NodeSpec = {
92
176
  content: "paragraph+",
93
177
  tableRole: "header_cell",
94
178
  isolating: true,
95
- attrs: {
96
- propertiesXml: { default: null },
97
- gridSpan: { default: 1 },
98
- verticalMerge: { default: null },
99
- colspan: { default: 1 },
100
- rowspan: { default: 1 },
101
- colwidth: { default: null },
102
- },
179
+ attrs: tableCellSpecAttrs,
103
180
  parseDOM: [
104
181
  {
105
182
  tag: "th",
106
- getAttrs(dom: HTMLElement) {
107
- const colspan = dom.getAttribute("colspan");
108
- const rowspan = dom.getAttribute("rowspan");
109
- return {
110
- colspan: colspan ? Number.parseInt(colspan, 10) : 1,
111
- rowspan: rowspan ? Number.parseInt(rowspan, 10) : 1,
112
- };
183
+ getAttrs(dom) {
184
+ if (!(dom instanceof HTMLElement)) {
185
+ return false;
186
+ }
187
+ return getCellAttrs(dom);
113
188
  },
114
189
  },
115
190
  ],
116
191
  toDOM(node) {
117
- const attrs: Record<string, string> = {
118
- class: "border border-primary/20 p-2 align-top font-semibold bg-surface-raised",
119
- };
120
- const colspan = resolveRenderedColspan(node.attrs as { colspan?: number; gridSpan?: number });
121
- if (colspan > 1) attrs.colspan = String(colspan);
122
- if (node.attrs.rowspan > 1) attrs.rowspan = String(node.attrs.rowspan);
123
- return ["th", attrs, 0];
192
+ return [
193
+ "th",
194
+ setCellDomAttrs(
195
+ node.attrs as TableCellAttrs,
196
+ "border border-primary/20 p-2 align-top font-semibold bg-surface-raised",
197
+ ),
198
+ 0,
199
+ ];
124
200
  },
125
201
  };
126
202
 
@@ -880,6 +880,8 @@ function findRegionFocusTarget(
880
880
  if (regionId === "document" || regionId === "toolbar" || regionId === "review-rail" || regionId === "status") {
881
881
  return region;
882
882
  }
883
+
884
+ return null;
883
885
  }
884
886
 
885
887
  function isAccessibleRegionId(value: string | undefined): value is AccessibleRegionId {