@beyondwork/docx-react-component 1.0.11 → 1.0.13

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 (40) hide show
  1. package/README.md +8 -2
  2. package/package.json +35 -21
  3. package/src/api/public-types.ts +103 -1
  4. package/src/core/commands/formatting-commands.ts +742 -0
  5. package/src/core/commands/image-commands.ts +84 -2
  6. package/src/core/commands/structural-helpers.ts +309 -0
  7. package/src/core/commands/table-structure-commands.ts +721 -0
  8. package/src/core/commands/text-commands.ts +166 -1
  9. package/src/core/state/editor-state.ts +318 -9
  10. package/src/formats/xlsx/io/parse-sheet.ts +177 -7
  11. package/src/formats/xlsx/io/parse-styles.ts +2 -0
  12. package/src/formats/xlsx/io/xlsx-session.ts +18 -12
  13. package/src/formats/xlsx/model/sheet.ts +81 -1
  14. package/src/formats/xlsx/model/workbook.ts +10 -6
  15. package/src/io/docx-session.ts +392 -22
  16. package/src/io/export/export-session.ts +55 -0
  17. package/src/io/export/serialize-footnotes.ts +5 -20
  18. package/src/io/export/serialize-headers-footers.ts +5 -31
  19. package/src/io/export/serialize-main-document.ts +78 -5
  20. package/src/io/normalize/normalize-text.ts +90 -1
  21. package/src/io/ooxml/parse-footnotes.ts +68 -5
  22. package/src/io/ooxml/parse-headers-footers.ts +67 -9
  23. package/src/io/ooxml/parse-main-document.ts +169 -6
  24. package/src/io/opc/package-reader.ts +3 -3
  25. package/src/io/source-package-provenance.ts +241 -0
  26. package/src/model/canonical-document.ts +450 -2
  27. package/src/model/cds-1.0.0.ts +5 -2
  28. package/src/model/snapshot.ts +190 -19
  29. package/src/preservation/package-preservation.ts +0 -7
  30. package/src/runtime/document-runtime.ts +7 -1
  31. package/src/runtime/read-only-diagnostics-runtime.ts +1 -1
  32. package/src/runtime/surface-projection.ts +200 -17
  33. package/src/runtime/table-commands.ts +79 -0
  34. package/src/runtime/table-schema.ts +9 -0
  35. package/src/ui/WordReviewEditor.tsx +708 -16
  36. package/src/ui-tailwind/editor-surface/pm-schema.ts +121 -5
  37. package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +73 -7
  38. package/src/ui-tailwind/editor-surface/search-plugin.ts +76 -16
  39. package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +162 -14
  40. package/src/validation/compatibility-engine.ts +208 -0
@@ -1,4 +1,4 @@
1
- import { decodeXmlEntities, parseSharedStringsXml as _unused } from "./parse-shared-strings.ts";
1
+ import { decodeXmlEntities } from "./parse-shared-strings.ts";
2
2
  import { parseXmlAttributes } from "./parse-styles.ts";
3
3
 
4
4
  // Re-export for external use
@@ -12,11 +12,23 @@ export { decodeXmlEntities };
12
12
  * b (boolean), e (error), str (formula string result), and blank.
13
13
  */
14
14
  export type XlsxParsedCellValue =
15
+ | { type: "blank" }
16
+ | { type: "text"; value: string; fromSharedString: boolean }
17
+ | { type: "number"; value: number }
18
+ | { type: "boolean"; value: boolean }
19
+ | {
20
+ type: "formula";
21
+ formula: string;
22
+ referenceTokens: string[];
23
+ cachedValue: XlsxParsedFormulaCachedValue | null;
24
+ }
25
+ | { type: "error"; errorCode: string };
26
+
27
+ export type XlsxParsedFormulaCachedValue =
15
28
  | { type: "blank" }
16
29
  | { type: "text"; value: string }
17
30
  | { type: "number"; value: number }
18
31
  | { type: "boolean"; value: boolean }
19
- | { type: "formula"; formula: string; cachedValue: string | null }
20
32
  | { type: "error"; errorCode: string };
21
33
 
22
34
  export interface XlsxParsedCell {
@@ -37,9 +49,17 @@ export interface XlsxParsedMerge {
37
49
  endCol: number;
38
50
  }
39
51
 
52
+ export interface XlsxParsedDimension {
53
+ startRow: number;
54
+ startCol: number;
55
+ endRow: number;
56
+ endCol: number;
57
+ }
58
+
40
59
  export interface SheetParseResult {
41
60
  cells: XlsxParsedCell[];
42
61
  merges: XlsxParsedMerge[];
62
+ dimension: XlsxParsedDimension | null;
43
63
  }
44
64
 
45
65
  /**
@@ -54,6 +74,7 @@ export function parseSheetXml(
54
74
  ): SheetParseResult {
55
75
  const cells: XlsxParsedCell[] = [];
56
76
  const merges: XlsxParsedMerge[] = [];
77
+ const dimension = parseSheetDimension(xml);
57
78
 
58
79
  const sheetDataMatch = /<sheetData>([\s\S]*?)<\/sheetData>/i.exec(xml);
59
80
  if (sheetDataMatch) {
@@ -65,7 +86,7 @@ export function parseSheetXml(
65
86
  parseMergeCells(mergeCellsMatch[1] ?? "", merges);
66
87
  }
67
88
 
68
- return { cells, merges };
89
+ return { cells, merges, dimension };
69
90
  }
70
91
 
71
92
  function parseSheetData(
@@ -145,10 +166,12 @@ function resolveCellValue(
145
166
  ): XlsxParsedCellValue {
146
167
  // Formula cells may have any result type; store formula + cached value.
147
168
  if (formulaText !== null) {
169
+ const decodedFormula = decodeXmlEntities(formulaText);
148
170
  return {
149
171
  type: "formula",
150
- formula: decodeXmlEntities(formulaText),
151
- cachedValue: rawValue !== null ? decodeXmlEntities(rawValue) : null,
172
+ formula: decodedFormula,
173
+ referenceTokens: extractFormulaReferenceTokens(decodedFormula),
174
+ cachedValue: resolveFormulaCachedValue(typeCode, rawValue, inlineText, sharedStrings),
152
175
  };
153
176
  }
154
177
 
@@ -160,11 +183,11 @@ function resolveCellValue(
160
183
  !Number.isNaN(index) && index >= 0 && index < sharedStrings.length
161
184
  ? (sharedStrings[index] ?? "")
162
185
  : "";
163
- return { type: "text", value: text };
186
+ return { type: "text", value: text, fromSharedString: true };
164
187
  }
165
188
 
166
189
  case "inlineStr": {
167
- return { type: "text", value: inlineText ?? "" };
190
+ return { type: "text", value: inlineText ?? "", fromSharedString: false };
168
191
  }
169
192
 
170
193
  case "str": {
@@ -173,6 +196,7 @@ function resolveCellValue(
173
196
  return {
174
197
  type: "text",
175
198
  value: rawValue !== null ? decodeXmlEntities(rawValue) : "",
199
+ fromSharedString: false,
176
200
  };
177
201
  }
178
202
 
@@ -202,6 +226,61 @@ function resolveCellValue(
202
226
  }
203
227
  }
204
228
 
229
+ function resolveFormulaCachedValue(
230
+ typeCode: string,
231
+ rawValue: string | null,
232
+ inlineText: string | null,
233
+ sharedStrings: readonly string[],
234
+ ): XlsxParsedFormulaCachedValue | null {
235
+ switch (typeCode) {
236
+ case "s": {
237
+ const index = rawValue !== null ? parseInt(rawValue, 10) : NaN;
238
+ return {
239
+ type: "text",
240
+ value:
241
+ !Number.isNaN(index) && index >= 0 && index < sharedStrings.length
242
+ ? (sharedStrings[index] ?? "")
243
+ : "",
244
+ };
245
+ }
246
+
247
+ case "inlineStr":
248
+ return { type: "text", value: inlineText ?? "" };
249
+
250
+ case "str":
251
+ return rawValue === null
252
+ ? { type: "blank" }
253
+ : { type: "text", value: decodeXmlEntities(rawValue) };
254
+
255
+ case "b":
256
+ return rawValue === null
257
+ ? { type: "blank" }
258
+ : { type: "boolean", value: rawValue === "1" };
259
+
260
+ case "e":
261
+ return rawValue === null
262
+ ? { type: "blank" }
263
+ : { type: "error", errorCode: decodeXmlEntities(rawValue) };
264
+
265
+ default: {
266
+ if (rawValue === null || rawValue === "") {
267
+ return { type: "blank" };
268
+ }
269
+
270
+ const numericValue = Number(rawValue);
271
+ if (!Number.isNaN(numericValue)) {
272
+ return { type: "number", value: numericValue };
273
+ }
274
+
275
+ if (rawValue === "TRUE" || rawValue === "FALSE") {
276
+ return { type: "boolean", value: rawValue === "TRUE" };
277
+ }
278
+
279
+ return { type: "text", value: decodeXmlEntities(rawValue) };
280
+ }
281
+ }
282
+ }
283
+
205
284
  function extractTextContent(xml: string, tagName: string): string | null {
206
285
  const pattern = new RegExp(`<${tagName}(?:\\s[^>]*)?>([^<]*)</${tagName}>`, "i");
207
286
  const match = pattern.exec(xml);
@@ -260,6 +339,97 @@ function parseMergeRef(ref: string): XlsxParsedMerge | null {
260
339
  };
261
340
  }
262
341
 
342
+ function parseSheetDimension(xml: string): XlsxParsedDimension | null {
343
+ const dimensionMatch = /<dimension\b([^>]*?)(?:\/>|>)/i.exec(xml);
344
+ if (!dimensionMatch) {
345
+ return null;
346
+ }
347
+
348
+ const attrs = parseXmlAttributes(dimensionMatch[1] ?? "");
349
+ const ref = attrs["ref"];
350
+ if (!ref) {
351
+ return null;
352
+ }
353
+
354
+ return parseDimensionRef(ref);
355
+ }
356
+
357
+ const SHEET_PREFIX_PATTERN =
358
+ "(?:'(?:[^']|'')+'|[A-Za-z_][A-Za-z0-9_.]*)!";
359
+ const CELL_REFERENCE_PATTERN = "\\$?[A-Z]{1,3}\\$?[1-9][0-9]*";
360
+ const RANGE_REFERENCE_PATTERN =
361
+ `${CELL_REFERENCE_PATTERN}(?::${CELL_REFERENCE_PATTERN})?`;
362
+ const FORMULA_REFERENCE_PATTERN = new RegExp(
363
+ String.raw`(?<![A-Za-z0-9_.$])(?:${SHEET_PREFIX_PATTERN})?${RANGE_REFERENCE_PATTERN}(?![A-Za-z0-9_])`,
364
+ "g",
365
+ );
366
+
367
+ function extractFormulaReferenceTokens(formula: string): string[] {
368
+ const maskedFormula = maskQuotedFormulaStrings(formula);
369
+ const tokens: string[] = [];
370
+ const seen = new Set<string>();
371
+
372
+ for (const match of maskedFormula.matchAll(FORMULA_REFERENCE_PATTERN)) {
373
+ const index = match.index ?? -1;
374
+ if (index < 0) {
375
+ continue;
376
+ }
377
+ const token = formula.slice(index, index + match[0].length);
378
+ if (seen.has(token)) {
379
+ continue;
380
+ }
381
+ seen.add(token);
382
+ tokens.push(token);
383
+ }
384
+
385
+ return tokens;
386
+ }
387
+
388
+ function maskQuotedFormulaStrings(formula: string): string {
389
+ let output = "";
390
+ let inString = false;
391
+
392
+ for (let index = 0; index < formula.length; index += 1) {
393
+ const char = formula[index];
394
+ const next = formula[index + 1];
395
+
396
+ if (char === "\"") {
397
+ output += char;
398
+ if (inString && next === "\"") {
399
+ output += " ";
400
+ index += 1;
401
+ continue;
402
+ }
403
+ inString = !inString;
404
+ continue;
405
+ }
406
+
407
+ output += inString ? " " : char;
408
+ }
409
+
410
+ return output;
411
+ }
412
+
413
+ function parseDimensionRef(ref: string): XlsxParsedDimension | null {
414
+ const [startRef, endRef = startRef] = ref.split(":");
415
+ if (!startRef || !endRef) {
416
+ return null;
417
+ }
418
+
419
+ const start = parseCellRef(startRef);
420
+ const end = parseCellRef(endRef);
421
+ if (start === null || end === null) {
422
+ return null;
423
+ }
424
+
425
+ return {
426
+ startRow: start.row,
427
+ startCol: start.col,
428
+ endRow: end.row,
429
+ endCol: end.col,
430
+ };
431
+ }
432
+
263
433
  function parseCellRef(ref: string): { row: number; col: number } | null {
264
434
  const match = /^([A-Z]+)([0-9]+)$/i.exec(ref.trim());
265
435
  if (!match) {
@@ -9,6 +9,7 @@ export interface XlsxStyleEntry {
9
9
  fontId: number;
10
10
  fillId: number;
11
11
  borderId: number;
12
+ rawAttributes: Record<string, string>;
12
13
  }
13
14
 
14
15
  export function parseStylesXml(xml: string): XlsxStyleEntry[] {
@@ -29,6 +30,7 @@ export function parseStylesXml(xml: string): XlsxStyleEntry[] {
29
30
  fontId: parseIntAttr(attrs["fontId"], 0),
30
31
  fillId: parseIntAttr(attrs["fillId"], 0),
31
32
  borderId: parseIntAttr(attrs["borderId"], 0),
33
+ rawAttributes: attrs,
32
34
  });
33
35
  }
34
36
 
@@ -33,7 +33,7 @@ import { parseWorkbookXml } from "./parse-workbook.ts";
33
33
  import { parseSharedStringsXml } from "./parse-shared-strings.ts";
34
34
  import { parseStylesXml, parseXmlAttributes } from "./parse-styles.ts";
35
35
  import { parseSheetXml } from "./parse-sheet.ts";
36
- import type { XlsxParsedCellValue } from "./parse-sheet.ts";
36
+ import type { XlsxParsedCellValue, XlsxParsedFormulaCachedValue } from "./parse-sheet.ts";
37
37
 
38
38
  // ---------------------------------------------------------------------------
39
39
  // Relationship type constants (SpreadsheetML / OPC)
@@ -177,7 +177,7 @@ function buildStyleRegistry(stylesXml: string): WorkbookStyleRegistry {
177
177
  fillId: entry.fillId,
178
178
  borderId: entry.borderId,
179
179
  numFmtId: entry.numFmtId,
180
- rawAttributes: {},
180
+ rawAttributes: entry.rawAttributes,
181
181
  }),
182
182
  );
183
183
 
@@ -239,7 +239,7 @@ function convertParsedCellValue(
239
239
  return styleRef ? makeBlankCell(styleRef) : null;
240
240
 
241
241
  case "text":
242
- return makeTextCell(parsed.value, false, styleRef);
242
+ return makeTextCell(parsed.value, parsed.fromSharedString, styleRef);
243
243
 
244
244
  case "number":
245
245
  return makeNumberCell(parsed.value, styleRef);
@@ -255,6 +255,7 @@ function convertParsedCellValue(
255
255
  parsed.formula,
256
256
  convertCachedFormulaValue(parsed.cachedValue),
257
257
  styleRef,
258
+ parsed.referenceTokens,
258
259
  );
259
260
  }
260
261
  }
@@ -282,19 +283,24 @@ function normalizeErrorCode(raw: string): CellErrorCode {
282
283
  * parse layer does not currently surface for formula cells.
283
284
  */
284
285
  function convertCachedFormulaValue(
285
- raw: string | null,
286
+ cachedValue: XlsxParsedFormulaCachedValue | null,
286
287
  ): CachedFormulaValue | undefined {
287
- if (raw === null) {
288
+ if (cachedValue === null) {
288
289
  return undefined;
289
290
  }
290
- const num = Number(raw);
291
- if (!Number.isNaN(num)) {
292
- return { type: "number", value: num };
293
- }
294
- if (raw === "TRUE" || raw === "FALSE") {
295
- return { type: "boolean", value: raw === "TRUE" };
291
+
292
+ switch (cachedValue.type) {
293
+ case "blank":
294
+ return { type: "blank" };
295
+ case "number":
296
+ return { type: "number", value: cachedValue.value };
297
+ case "text":
298
+ return { type: "text", value: cachedValue.value };
299
+ case "boolean":
300
+ return { type: "boolean", value: cachedValue.value };
301
+ case "error":
302
+ return { type: "error", errorCode: normalizeErrorCode(cachedValue.errorCode) };
296
303
  }
297
- return { type: "text", value: raw };
298
304
  }
299
305
 
300
306
  // ---------------------------------------------------------------------------
@@ -5,7 +5,7 @@
5
5
  * explicit style are stored. Row and column metadata is also sparse.
6
6
  */
7
7
 
8
- import type { CellAddress, CellKey, CellValue, ColIndex, RowIndex } from "./cell.ts";
8
+ import type { CellAddress, CellKey, CellValue, ColIndex, RowIndex, StyleRef } from "./cell.ts";
9
9
  import { cellKey } from "./cell.ts";
10
10
 
11
11
  export type { ColIndex, RowIndex } from "./cell.ts";
@@ -23,6 +23,8 @@ export interface RowProperties {
23
23
  hidden?: boolean;
24
24
  /** Whether the row height was set via customHeight attribute. */
25
25
  customHeight?: boolean;
26
+ /** Style index reference applied at the row level, if present. */
27
+ styleRef?: StyleRef;
26
28
  }
27
29
 
28
30
  export interface ColProperties {
@@ -34,6 +36,8 @@ export interface ColProperties {
34
36
  hidden?: boolean;
35
37
  /** Whether the width was explicitly customized. */
36
38
  customWidth?: boolean;
39
+ /** Style index reference applied at the column level, if present. */
40
+ styleRef?: StyleRef;
37
41
  }
38
42
 
39
43
  // ---------------------------------------------------------------------------
@@ -164,6 +168,60 @@ export function getCell(
164
168
  return sheet.cells.get(cellKey(row, col));
165
169
  }
166
170
 
171
+ /**
172
+ * Set sparse row metadata.
173
+ * Passing `undefined`, or only default fields, removes the row entry.
174
+ */
175
+ export function setRowProperties(
176
+ sheet: CanonicalSheet,
177
+ rowIndex: RowIndex,
178
+ props: Omit<RowProperties, "rowIndex"> | undefined,
179
+ ): void {
180
+ if (!props || isDefaultRowProperties(props)) {
181
+ sheet.rowProps.delete(rowIndex);
182
+ return;
183
+ }
184
+
185
+ sheet.rowProps.set(rowIndex, {
186
+ rowIndex,
187
+ ...props,
188
+ });
189
+ }
190
+
191
+ export function getRowProperties(
192
+ sheet: CanonicalSheet,
193
+ rowIndex: RowIndex,
194
+ ): RowProperties | undefined {
195
+ return sheet.rowProps.get(rowIndex);
196
+ }
197
+
198
+ /**
199
+ * Set sparse column metadata.
200
+ * Passing `undefined`, or only default fields, removes the column entry.
201
+ */
202
+ export function setColProperties(
203
+ sheet: CanonicalSheet,
204
+ colIndex: ColIndex,
205
+ props: Omit<ColProperties, "colIndex"> | undefined,
206
+ ): void {
207
+ if (!props || isDefaultColProperties(props)) {
208
+ sheet.colProps.delete(colIndex);
209
+ return;
210
+ }
211
+
212
+ sheet.colProps.set(colIndex, {
213
+ colIndex,
214
+ ...props,
215
+ });
216
+ }
217
+
218
+ export function getColProperties(
219
+ sheet: CanonicalSheet,
220
+ colIndex: ColIndex,
221
+ ): ColProperties | undefined {
222
+ return sheet.colProps.get(colIndex);
223
+ }
224
+
167
225
  /**
168
226
  * Return the effective address bounds for all occupied cells.
169
227
  * Returns `null` when the sheet has no cells.
@@ -244,3 +302,25 @@ export function findMergesContaining(
244
302
  col <= m.endCol,
245
303
  );
246
304
  }
305
+
306
+ function isDefaultRowProperties(
307
+ props: Omit<RowProperties, "rowIndex">,
308
+ ): boolean {
309
+ return (
310
+ props.heightPt === undefined &&
311
+ props.hidden === undefined &&
312
+ props.customHeight === undefined &&
313
+ props.styleRef === undefined
314
+ );
315
+ }
316
+
317
+ function isDefaultColProperties(
318
+ props: Omit<ColProperties, "colIndex">,
319
+ ): boolean {
320
+ return (
321
+ props.widthChars === undefined &&
322
+ props.hidden === undefined &&
323
+ props.customWidth === undefined &&
324
+ props.styleRef === undefined
325
+ );
326
+ }
@@ -158,15 +158,15 @@ export function addSheet(
158
158
  }
159
159
  const orderIndex =
160
160
  atIndex !== undefined
161
- ? Math.min(atIndex, workbook.sheetOrder.length)
161
+ ? clampSheetIndex(atIndex, workbook.sheetOrder.length)
162
162
  : workbook.sheetOrder.length;
163
163
  const sheet = createSheet(sheetId, name, orderIndex);
164
164
  workbook.sheets.set(sheetId, sheet);
165
165
 
166
- if (atIndex !== undefined && atIndex < workbook.sheetOrder.length) {
167
- workbook.sheetOrder.splice(atIndex, 0, sheetId);
166
+ if (atIndex !== undefined && orderIndex < workbook.sheetOrder.length) {
167
+ workbook.sheetOrder.splice(orderIndex, 0, sheetId);
168
168
  // Reindex sheets after the insertion point
169
- for (let i = atIndex + 1; i < workbook.sheetOrder.length; i++) {
169
+ for (let i = orderIndex + 1; i < workbook.sheetOrder.length; i++) {
170
170
  const id = workbook.sheetOrder[i];
171
171
  const s = workbook.sheets.get(id);
172
172
  if (s) s.orderIndex = i;
@@ -258,10 +258,10 @@ export function moveSheet(
258
258
  ): void {
259
259
  const currentIndex = workbook.sheetOrder.indexOf(sheetId);
260
260
  if (currentIndex === -1) throw new Error(`Sheet not found: ${sheetId}`);
261
- if (currentIndex === toIndex) return;
261
+ const clampedTarget = clampSheetIndex(toIndex, workbook.sheetOrder.length - 1);
262
+ if (currentIndex === clampedTarget) return;
262
263
 
263
264
  workbook.sheetOrder.splice(currentIndex, 1);
264
- const clampedTarget = Math.min(toIndex, workbook.sheetOrder.length);
265
265
  workbook.sheetOrder.splice(clampedTarget, 0, sheetId);
266
266
 
267
267
  // Reindex all sheets
@@ -272,6 +272,10 @@ export function moveSheet(
272
272
  }
273
273
  }
274
274
 
275
+ function clampSheetIndex(index: number, maxIndex: number): number {
276
+ return Math.max(0, Math.min(index, maxIndex));
277
+ }
278
+
275
279
  // ---------------------------------------------------------------------------
276
280
  // Shared string table helpers
277
281
  // ---------------------------------------------------------------------------