@beyondwork/docx-react-component 1.0.12 → 1.0.14
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/package.json +9 -1
- package/src/api/public-types.ts +72 -0
- package/src/core/commands/formatting-commands.ts +742 -0
- package/src/core/commands/image-commands.ts +84 -2
- package/src/core/commands/structural-helpers.ts +309 -0
- package/src/core/commands/table-structure-commands.ts +721 -0
- package/src/core/commands/text-commands.ts +166 -1
- package/src/core/selection/review-anchors.ts +89 -0
- package/src/formats/xlsx/io/parse-sheet.ts +177 -7
- package/src/formats/xlsx/io/parse-styles.ts +2 -0
- package/src/formats/xlsx/io/xlsx-session.ts +18 -12
- package/src/formats/xlsx/model/sheet.ts +81 -1
- package/src/formats/xlsx/model/workbook.ts +10 -6
- package/src/runtime/document-runtime.ts +13 -0
- package/src/runtime/session-capabilities.ts +22 -1
- package/src/runtime/surface-projection.ts +1 -0
- package/src/runtime/table-commands.ts +79 -0
- package/src/runtime/table-schema.ts +9 -0
- package/src/ui/WordReviewEditor.tsx +534 -8
- package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +8 -2
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +7 -5
- package/src/ui-tailwind/editor-surface/search-plugin.ts +76 -16
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +162 -14
- package/src/ui-tailwind/tw-review-workspace.tsx +3 -0
|
@@ -1,8 +1,23 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { InsertTableOptions } from "../../api/public-types";
|
|
2
|
+
import type { ParagraphNode } from "../../model/canonical-document.ts";
|
|
3
|
+
import {
|
|
4
|
+
createSelectionSnapshot,
|
|
5
|
+
type CanonicalDocumentEnvelope,
|
|
6
|
+
type SelectionSnapshot,
|
|
7
|
+
} from "../state/editor-state.ts";
|
|
2
8
|
import {
|
|
3
9
|
applyTextTransaction,
|
|
4
10
|
type TextTransactionResult,
|
|
5
11
|
} from "../state/text-transaction.ts";
|
|
12
|
+
import {
|
|
13
|
+
createInsertedTableBlock,
|
|
14
|
+
createNoopStructuralMutation,
|
|
15
|
+
findTableCellParagraphSelection,
|
|
16
|
+
replaceParagraphScope,
|
|
17
|
+
resolveInsertedTableStyleId,
|
|
18
|
+
resolveParagraphScope,
|
|
19
|
+
type StructuralMutationResult,
|
|
20
|
+
} from "./structural-helpers.ts";
|
|
6
21
|
|
|
7
22
|
export interface TextCommandContext {
|
|
8
23
|
timestamp: string;
|
|
@@ -117,3 +132,153 @@ export function splitParagraph(
|
|
|
117
132
|
context,
|
|
118
133
|
);
|
|
119
134
|
}
|
|
135
|
+
|
|
136
|
+
export function insertPageBreak(
|
|
137
|
+
document: CanonicalDocumentEnvelope,
|
|
138
|
+
selection: SelectionSnapshot,
|
|
139
|
+
context: TextCommandContext,
|
|
140
|
+
): StructuralMutationResult {
|
|
141
|
+
const scope = resolveParagraphScope(document, selection);
|
|
142
|
+
if (!scope) {
|
|
143
|
+
return createNoopStructuralMutation(document, selection);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const localDocument: CanonicalDocumentEnvelope = {
|
|
147
|
+
...document,
|
|
148
|
+
content: {
|
|
149
|
+
type: "doc",
|
|
150
|
+
children: [scope.paragraph],
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
const localSelection = createSelectionSnapshot(
|
|
154
|
+
selection.anchor - scope.paragraphStart,
|
|
155
|
+
selection.head - scope.paragraphStart,
|
|
156
|
+
);
|
|
157
|
+
const splitResult = splitParagraph(localDocument, localSelection, context);
|
|
158
|
+
const splitRoot = splitResult.document.content;
|
|
159
|
+
if (!splitRoot || splitRoot.type !== "doc") {
|
|
160
|
+
return createNoopStructuralMutation(document, selection);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const replacementParagraphs = splitRoot.children
|
|
164
|
+
.filter((block): block is ParagraphNode => block.type === "paragraph")
|
|
165
|
+
.map((block, index) =>
|
|
166
|
+
index === 1
|
|
167
|
+
? {
|
|
168
|
+
...block,
|
|
169
|
+
pageBreakBefore: true,
|
|
170
|
+
}
|
|
171
|
+
: block,
|
|
172
|
+
);
|
|
173
|
+
if (replacementParagraphs.length < 2) {
|
|
174
|
+
return createNoopStructuralMutation(document, selection);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const nextDocument = replaceParagraphScope(document, scope, replacementParagraphs);
|
|
178
|
+
return {
|
|
179
|
+
changed: true,
|
|
180
|
+
document: {
|
|
181
|
+
...nextDocument,
|
|
182
|
+
updatedAt: context.timestamp,
|
|
183
|
+
},
|
|
184
|
+
selection: createSelectionSnapshot(
|
|
185
|
+
splitResult.selection.anchor + scope.paragraphStart,
|
|
186
|
+
splitResult.selection.head + scope.paragraphStart,
|
|
187
|
+
),
|
|
188
|
+
mapping: {
|
|
189
|
+
...splitResult.mapping,
|
|
190
|
+
steps: splitResult.mapping.steps.map((step) => ({
|
|
191
|
+
...step,
|
|
192
|
+
from: step.from + scope.paragraphStart,
|
|
193
|
+
to: step.to + scope.paragraphStart,
|
|
194
|
+
})),
|
|
195
|
+
},
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function insertTable(
|
|
200
|
+
document: CanonicalDocumentEnvelope,
|
|
201
|
+
selection: SelectionSnapshot,
|
|
202
|
+
options: InsertTableOptions,
|
|
203
|
+
context: TextCommandContext,
|
|
204
|
+
): StructuralMutationResult {
|
|
205
|
+
const scope = resolveParagraphScope(document, selection);
|
|
206
|
+
if (!scope || scope.kind !== "top-level") {
|
|
207
|
+
return createNoopStructuralMutation(document, selection);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const safeRows = Math.max(1, Math.floor(options.rows));
|
|
211
|
+
const safeColumns = Math.max(1, Math.floor(options.columns));
|
|
212
|
+
if (!Number.isFinite(safeRows) || !Number.isFinite(safeColumns)) {
|
|
213
|
+
return createNoopStructuralMutation(document, selection);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const localDocument: CanonicalDocumentEnvelope = {
|
|
217
|
+
...document,
|
|
218
|
+
content: {
|
|
219
|
+
type: "doc",
|
|
220
|
+
children: [scope.paragraph],
|
|
221
|
+
},
|
|
222
|
+
};
|
|
223
|
+
const localSelection = createSelectionSnapshot(
|
|
224
|
+
selection.anchor - scope.paragraphStart,
|
|
225
|
+
selection.head - scope.paragraphStart,
|
|
226
|
+
);
|
|
227
|
+
const splitResult = splitParagraph(localDocument, localSelection, context);
|
|
228
|
+
const splitRoot = splitResult.document.content;
|
|
229
|
+
if (!splitRoot || splitRoot.type !== "doc") {
|
|
230
|
+
return createNoopStructuralMutation(document, selection);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const replacementParagraphs = splitRoot.children.filter(
|
|
234
|
+
(block): block is ParagraphNode => block.type === "paragraph",
|
|
235
|
+
);
|
|
236
|
+
if (replacementParagraphs.length < 2) {
|
|
237
|
+
return createNoopStructuralMutation(document, selection);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const nextRoot = document.content;
|
|
241
|
+
if (!nextRoot || nextRoot.type !== "doc") {
|
|
242
|
+
return createNoopStructuralMutation(document, selection);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const insertedTable = createInsertedTableBlock(
|
|
246
|
+
safeRows,
|
|
247
|
+
safeColumns,
|
|
248
|
+
resolveInsertedTableStyleId(document),
|
|
249
|
+
);
|
|
250
|
+
const nextDocument: CanonicalDocumentEnvelope = {
|
|
251
|
+
...document,
|
|
252
|
+
updatedAt: context.timestamp,
|
|
253
|
+
content: {
|
|
254
|
+
...nextRoot,
|
|
255
|
+
children: [
|
|
256
|
+
...nextRoot.children.slice(0, scope.blockIndex),
|
|
257
|
+
replacementParagraphs[0],
|
|
258
|
+
insertedTable,
|
|
259
|
+
replacementParagraphs[1],
|
|
260
|
+
...nextRoot.children.slice(scope.blockIndex + 1),
|
|
261
|
+
],
|
|
262
|
+
},
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
return {
|
|
266
|
+
changed: true,
|
|
267
|
+
document: nextDocument,
|
|
268
|
+
selection:
|
|
269
|
+
findTableCellParagraphSelection(
|
|
270
|
+
nextDocument,
|
|
271
|
+
scope.blockIndex + 1,
|
|
272
|
+
0,
|
|
273
|
+
0,
|
|
274
|
+
) ?? selection,
|
|
275
|
+
mapping: {
|
|
276
|
+
...splitResult.mapping,
|
|
277
|
+
steps: splitResult.mapping.steps.map((step) => ({
|
|
278
|
+
...step,
|
|
279
|
+
from: step.from + scope.paragraphStart,
|
|
280
|
+
to: step.to + scope.paragraphStart,
|
|
281
|
+
})),
|
|
282
|
+
},
|
|
283
|
+
};
|
|
284
|
+
}
|
|
@@ -76,6 +76,16 @@ export function rangeStaysWithinSingleParagraph(
|
|
|
76
76
|
return true;
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
+
const surfaceBlocks = readSurfaceBlocks(content);
|
|
80
|
+
if (surfaceBlocks) {
|
|
81
|
+
return surfaceBlocks.some(
|
|
82
|
+
(block) =>
|
|
83
|
+
block.kind === "paragraph" &&
|
|
84
|
+
normalized.from >= block.from &&
|
|
85
|
+
normalized.to <= block.to,
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
79
89
|
const story = parseTextStory(content);
|
|
80
90
|
const upperBound = Math.min(normalized.to, story.units.length);
|
|
81
91
|
|
|
@@ -92,3 +102,82 @@ export function rangeStaysWithinSingleParagraph(
|
|
|
92
102
|
|
|
93
103
|
return true;
|
|
94
104
|
}
|
|
105
|
+
|
|
106
|
+
export function canCreateDocxCommentAnchor(
|
|
107
|
+
content: unknown,
|
|
108
|
+
anchor: ReviewAnchor,
|
|
109
|
+
): boolean {
|
|
110
|
+
if (anchor.kind !== "range") {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const normalized = normalizeRange(anchor.range);
|
|
115
|
+
if (normalized.from === normalized.to) {
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return rangeStaysWithinSingleParagraph(content, normalized);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function readSurfaceBlocks(
|
|
123
|
+
content: unknown,
|
|
124
|
+
): Array<{ kind: string; from: number; to: number }> | undefined {
|
|
125
|
+
if (!content || typeof content !== "object" || !("blocks" in content)) {
|
|
126
|
+
return undefined;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const blocks = (content as { blocks?: unknown }).blocks;
|
|
130
|
+
if (!Array.isArray(blocks)) {
|
|
131
|
+
return undefined;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const normalized = flattenSurfaceBlocks(blocks);
|
|
135
|
+
|
|
136
|
+
return normalized.length > 0 ? normalized : undefined;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function flattenSurfaceBlocks(
|
|
140
|
+
blocks: unknown[],
|
|
141
|
+
): Array<{ kind: string; from: number; to: number }> {
|
|
142
|
+
const flattened: Array<{ kind: string; from: number; to: number }> = [];
|
|
143
|
+
|
|
144
|
+
for (const block of blocks) {
|
|
145
|
+
if (
|
|
146
|
+
!block ||
|
|
147
|
+
typeof block !== "object" ||
|
|
148
|
+
typeof (block as { kind?: unknown }).kind !== "string" ||
|
|
149
|
+
typeof (block as { from?: unknown }).from !== "number" ||
|
|
150
|
+
typeof (block as { to?: unknown }).to !== "number"
|
|
151
|
+
) {
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
flattened.push({
|
|
156
|
+
kind: (block as { kind: string }).kind,
|
|
157
|
+
from: (block as { from: number }).from,
|
|
158
|
+
to: (block as { to: number }).to,
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
if (
|
|
162
|
+
(block as { kind: string }).kind === "table" &&
|
|
163
|
+
Array.isArray((block as { rows?: unknown }).rows)
|
|
164
|
+
) {
|
|
165
|
+
for (const row of (block as { rows: Array<{ cells?: unknown[] }> }).rows) {
|
|
166
|
+
for (const cell of row.cells ?? []) {
|
|
167
|
+
if (cell && typeof cell === "object" && Array.isArray((cell as { content?: unknown[] }).content)) {
|
|
168
|
+
flattened.push(...flattenSurfaceBlocks((cell as { content: unknown[] }).content));
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (
|
|
175
|
+
(block as { kind: string }).kind === "sdt_block" &&
|
|
176
|
+
Array.isArray((block as { children?: unknown[] }).children)
|
|
177
|
+
) {
|
|
178
|
+
flattened.push(...flattenSurfaceBlocks((block as { children: unknown[] }).children));
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return flattened;
|
|
183
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { decodeXmlEntities
|
|
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:
|
|
151
|
-
|
|
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,
|
|
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
|
-
|
|
286
|
+
cachedValue: XlsxParsedFormulaCachedValue | null,
|
|
286
287
|
): CachedFormulaValue | undefined {
|
|
287
|
-
if (
|
|
288
|
+
if (cachedValue === null) {
|
|
288
289
|
return undefined;
|
|
289
290
|
}
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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
|
// ---------------------------------------------------------------------------
|