@shbernal/ts-xlsx 3.0.0 → 3.2.0
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 +34 -24
- package/dist/bytes.d.ts +10 -2
- package/dist/bytes.js +22 -0
- package/dist/core/address.d.ts +96 -2
- package/dist/core/address.js +87 -33
- package/dist/core/array-formula-ranges.d.ts +33 -0
- package/dist/core/array-formula-ranges.js +62 -0
- package/dist/core/autofilter.d.ts +4 -3
- package/dist/core/autofilter.js +31 -30
- package/dist/core/axis-handle.d.ts +2 -0
- package/dist/core/axis-handle.js +5 -2
- package/dist/core/cell.d.ts +4 -3
- package/dist/core/cell.js +4 -6
- package/dist/core/clone.d.ts +29 -0
- package/dist/core/clone.js +24 -0
- package/dist/core/color-resolution.js +4 -6
- package/dist/core/column.d.ts +1 -0
- package/dist/core/column.js +6 -2
- package/dist/core/comment-thread.d.ts +7 -3
- package/dist/core/comment-thread.js +2 -1
- package/dist/core/conditional-formatting-overlay.d.ts +11 -1
- package/dist/core/conditional-formatting-overlay.js +30 -3
- package/dist/core/conditional-formatting.d.ts +107 -11
- package/dist/core/conditional-formatting.js +76 -19
- package/dist/core/containers.d.ts +12 -0
- package/dist/core/containers.js +5 -0
- package/dist/core/data-validation-overlay.d.ts +7 -1
- package/dist/core/data-validation-overlay.js +19 -4
- package/dist/core/data-validation.d.ts +22 -0
- package/dist/core/data-validation.js +30 -4
- package/dist/core/date-format.d.ts +20 -0
- package/dist/core/date-format.js +170 -0
- package/dist/core/date.d.ts +43 -8
- package/dist/core/date.js +19 -3
- package/dist/core/formula-references.d.ts +26 -0
- package/dist/core/formula-references.js +173 -0
- package/dist/core/formula-scan.d.ts +2 -0
- package/dist/core/formula-scan.js +58 -0
- package/dist/core/formula.d.ts +76 -17
- package/dist/core/formula.js +87 -80
- package/dist/core/function-values.d.ts +4 -0
- package/dist/core/function-values.js +58 -0
- package/dist/core/future-functions.d.ts +3 -0
- package/dist/core/{modern-functions.js → future-functions.js} +119 -88
- package/dist/core/grid-edits.d.ts +30 -3
- package/dist/core/grid-edits.js +248 -81
- package/dist/core/grid-shift.d.ts +58 -9
- package/dist/core/grid-shift.js +26 -4
- package/dist/core/hyperlink.d.ts +39 -0
- package/dist/core/hyperlink.js +88 -0
- package/dist/core/image.d.ts +49 -14
- package/dist/core/image.js +16 -5
- package/dist/core/internal.d.ts +10 -2
- package/dist/core/limits.d.ts +25 -0
- package/dist/core/limits.js +4 -0
- package/dist/core/merge.d.ts +18 -4
- package/dist/core/merge.js +33 -20
- package/dist/core/page-setup.d.ts +26 -11
- package/dist/core/pivot-table.d.ts +49 -10
- package/dist/core/pivot-table.js +112 -60
- package/dist/core/protection.d.ts +66 -4
- package/dist/core/protection.js +6 -11
- package/dist/core/range.js +34 -30
- package/dist/core/row-input.js +2 -2
- package/dist/core/row.d.ts +1 -0
- package/dist/core/row.js +6 -2
- package/dist/core/structured-reference.d.ts +7 -0
- package/dist/core/structured-reference.js +7 -0
- package/dist/core/style.d.ts +85 -70
- package/dist/core/style.js +92 -28
- package/dist/core/table-style.d.ts +1 -1
- package/dist/core/table-style.js +4 -4
- package/dist/core/table.d.ts +64 -16
- package/dist/core/table.js +87 -40
- package/dist/core/theme.d.ts +2 -0
- package/dist/core/theme.js +9 -5
- package/dist/core/value.d.ts +60 -28
- package/dist/core/value.js +48 -43
- package/dist/core/workbook-media.d.ts +34 -0
- package/dist/core/workbook-media.js +44 -0
- package/dist/core/workbook-styles.d.ts +83 -1
- package/dist/core/workbook-styles.js +4 -0
- package/dist/core/workbook-theme.js +5 -3
- package/dist/core/workbook-vba.js +3 -2
- package/dist/core/workbook.d.ts +41 -3
- package/dist/core/workbook.js +46 -41
- package/dist/core/worksheet-comments.d.ts +2 -1
- package/dist/core/worksheet-comments.js +20 -13
- package/dist/core/worksheet-merges.d.ts +49 -0
- package/dist/core/worksheet-merges.js +65 -0
- package/dist/core/worksheet-model.d.ts +10 -2
- package/dist/core/worksheet-model.js +28 -9
- package/dist/core/worksheet-pictures.d.ts +3 -3
- package/dist/core/worksheet-pictures.js +7 -6
- package/dist/core/worksheet.d.ts +165 -27
- package/dist/core/worksheet.js +206 -89
- package/dist/customui/ribbon.js +2 -1
- package/dist/entries/core.d.ts +21 -14
- package/dist/entries/core.js +14 -7
- package/dist/entries/errors.d.ts +1 -0
- package/dist/entries/errors.js +1 -0
- package/dist/entries/xlsx.d.ts +1 -1
- package/dist/errors.d.ts +64 -0
- package/dist/errors.js +22 -0
- package/dist/hex.d.ts +8 -0
- package/dist/hex.js +3 -0
- package/dist/io/cell-metadata/metadata.d.ts +42 -0
- package/dist/io/cell-metadata/metadata.js +35 -0
- package/dist/io/cell-metadata/rich-values.d.ts +11 -0
- package/dist/io/cell-metadata/rich-values.js +80 -0
- package/dist/io/csv/delimiter.d.ts +21 -0
- package/dist/io/csv/delimiter.js +18 -0
- package/dist/io/csv/errors.d.ts +14 -0
- package/dist/io/csv/errors.js +5 -0
- package/dist/io/csv/read.d.ts +6 -1
- package/dist/io/csv/read.js +51 -22
- package/dist/io/csv/write.d.ts +19 -5
- package/dist/io/csv/write.js +24 -44
- package/dist/io/opc/inflate.js +14 -0
- package/dist/io/opc/namespaces.d.ts +16 -0
- package/dist/io/opc/namespaces.js +4 -0
- package/dist/io/opc/part-paths.d.ts +9 -0
- package/dist/io/opc/part-paths.js +5 -5
- package/dist/io/opc/read-opc.d.ts +58 -14
- package/dist/io/opc/read-opc.js +49 -21
- package/dist/io/opc/read-options.d.ts +5 -1
- package/dist/io/opc/sniff-format.d.ts +3 -3
- package/dist/io/opc/sniff-format.js +2 -2
- package/dist/io/opc/strict-relationships.d.ts +2 -0
- package/dist/io/opc/strict-relationships.js +12 -0
- package/dist/io/read-policy/column-budget.d.ts +26 -0
- package/dist/io/read-policy/column-budget.js +18 -0
- package/dist/io/read-policy/read-repair.d.ts +74 -0
- package/dist/io/read-policy/read-repair.js +119 -0
- package/dist/io/style/cell-style-resolution.d.ts +25 -0
- package/dist/io/style/cell-style-resolution.js +30 -0
- package/dist/io/style/xf-style.d.ts +23 -1
- package/dist/io/style/xf-style.js +8 -0
- package/dist/io/xlsb/formula.d.ts +2 -2
- package/dist/io/xlsb/formula.js +52 -35
- package/dist/io/xlsb/primitives.js +8 -14
- package/dist/io/xlsb/ptg-functions.d.ts +19 -10
- package/dist/io/xlsb/ptg-functions.js +495 -596
- package/dist/io/xlsb/read-metadata.d.ts +7 -0
- package/dist/io/xlsb/read-metadata.js +69 -0
- package/dist/io/xlsb/read-styles.js +45 -81
- package/dist/io/xlsb/read-worksheet.d.ts +26 -1
- package/dist/io/xlsb/read-worksheet.js +148 -106
- package/dist/io/xlsb/read.d.ts +6 -3
- package/dist/io/xlsb/read.js +70 -31
- package/dist/io/xlsb/record-stream.d.ts +30 -0
- package/dist/io/xlsb/record-stream.js +20 -0
- package/dist/io/xlsb/record-types.d.ts +14 -5
- package/dist/io/xlsb/record-types.js +14 -5
- package/dist/io/xlsb/sheet-protection.d.ts +20 -0
- package/dist/io/xlsb/sheet-protection.js +60 -0
- package/dist/io/xlsx/cell-accumulator.d.ts +24 -9
- package/dist/io/xlsx/cell-accumulator.js +74 -21
- package/dist/io/xlsx/cell-metadata.d.ts +39 -0
- package/dist/io/xlsx/cell-metadata.js +162 -0
- package/dist/io/xlsx/cell-value.d.ts +19 -5
- package/dist/io/xlsx/cell-value.js +45 -30
- package/dist/io/xlsx/color-xml.js +3 -2
- package/dist/io/xlsx/column-span.d.ts +11 -0
- package/dist/io/xlsx/column-span.js +9 -0
- package/dist/io/xlsx/comments.d.ts +9 -30
- package/dist/io/xlsx/comments.js +25 -83
- package/dist/io/xlsx/conditional-formatting.d.ts +43 -23
- package/dist/io/xlsx/conditional-formatting.js +437 -138
- package/dist/io/xlsx/data-validation.d.ts +12 -7
- package/dist/io/xlsx/data-validation.js +67 -53
- package/dist/io/xlsx/edit-vba.d.ts +7 -2
- package/dist/io/xlsx/edit-vba.js +52 -52
- package/dist/io/xlsx/font-xml.d.ts +7 -0
- package/dist/io/xlsx/font-xml.js +104 -0
- package/dist/io/xlsx/hyperlinks.d.ts +19 -21
- package/dist/io/xlsx/hyperlinks.js +12 -32
- package/dist/io/xlsx/images.d.ts +49 -16
- package/dist/io/xlsx/images.js +149 -50
- package/dist/io/xlsx/namespaces.d.ts +24 -0
- package/dist/io/xlsx/namespaces.js +11 -0
- package/dist/io/xlsx/package-plan.d.ts +60 -15
- package/dist/io/xlsx/package-plan.js +83 -18
- package/dist/io/xlsx/part-names.d.ts +6 -8
- package/dist/io/xlsx/part-names.js +3 -7
- package/dist/io/xlsx/pivot.d.ts +5 -5
- package/dist/io/xlsx/pivot.js +26 -24
- package/dist/io/xlsx/preserved-splices.d.ts +6 -0
- package/dist/io/xlsx/preserved-splices.js +74 -0
- package/dist/io/xlsx/read-comments.d.ts +29 -0
- package/dist/io/xlsx/read-comments.js +75 -0
- package/dist/io/xlsx/read-parts.d.ts +59 -0
- package/dist/io/xlsx/read-parts.js +259 -0
- package/dist/io/xlsx/read-pivot.js +11 -4
- package/dist/io/xlsx/{rich-runs.d.ts → read-rich-runs.d.ts} +8 -5
- package/dist/io/xlsx/{rich-runs.js → read-rich-runs.js} +24 -4
- package/dist/io/xlsx/read-rows.d.ts +35 -11
- package/dist/io/xlsx/read-rows.js +73 -49
- package/dist/io/xlsx/read-shared-strings.js +3 -2
- package/dist/io/xlsx/read-styles.d.ts +0 -30
- package/dist/io/xlsx/read-styles.js +45 -105
- package/dist/io/xlsx/read-workbook-xml.d.ts +42 -0
- package/dist/io/xlsx/read-workbook-xml.js +175 -0
- package/dist/io/xlsx/read-worksheet.d.ts +9 -3
- package/dist/io/xlsx/read-worksheet.js +70 -177
- package/dist/io/xlsx/read.d.ts +3 -14
- package/dist/io/xlsx/read.js +74 -411
- package/dist/io/xlsx/relationships.d.ts +3 -0
- package/dist/io/xlsx/relationships.js +4 -0
- package/dist/io/xlsx/rich-text.js +1 -1
- package/dist/io/xlsx/row-position.d.ts +20 -0
- package/dist/io/xlsx/row-position.js +11 -0
- package/dist/io/xlsx/row-xml.d.ts +139 -0
- package/dist/io/xlsx/row-xml.js +222 -0
- package/dist/io/xlsx/shared-formulas.js +2 -2
- package/dist/io/xlsx/sheet-properties.d.ts +10 -1
- package/dist/io/xlsx/sheet-properties.js +182 -20
- package/dist/io/xlsx/strict-parts.d.ts +7 -0
- package/dist/io/xlsx/strict-parts.js +171 -0
- package/dist/io/xlsx/style-elements.d.ts +41 -0
- package/dist/io/xlsx/style-elements.js +136 -0
- package/dist/io/xlsx/styles.d.ts +17 -3
- package/dist/io/xlsx/styles.js +13 -209
- package/dist/io/xlsx/tables.d.ts +10 -2
- package/dist/io/xlsx/tables.js +55 -52
- package/dist/io/xlsx/theme-xml.d.ts +32 -4
- package/dist/io/xlsx/theme-xml.js +106 -71
- package/dist/io/xlsx/threaded-comments.d.ts +14 -11
- package/dist/io/xlsx/threaded-comments.js +16 -8
- package/dist/io/xlsx/workbook-xml.d.ts +45 -6
- package/dist/io/xlsx/workbook-xml.js +62 -54
- package/dist/io/xlsx/worksheet-xml.d.ts +26 -70
- package/dist/io/xlsx/worksheet-xml.js +69 -282
- package/dist/io/xlsx/write-stream.d.ts +90 -19
- package/dist/io/xlsx/write-stream.js +80 -25
- package/dist/io/xlsx/write.d.ts +12 -6
- package/dist/io/xlsx/write.js +226 -123
- package/dist/rel-type.d.ts +25 -0
- package/dist/rel-type.js +9 -0
- package/dist/token-set.d.ts +13 -0
- package/dist/token-set.js +4 -1
- package/dist/vba/bytes.d.ts +16 -5
- package/dist/vba/bytes.js +27 -6
- package/dist/vba/cfb-format.d.ts +55 -0
- package/dist/vba/cfb-format.js +35 -0
- package/dist/vba/cfb-writer.d.ts +8 -0
- package/dist/vba/cfb-writer.js +40 -40
- package/dist/vba/cfb.d.ts +21 -4
- package/dist/vba/cfb.js +124 -90
- package/dist/vba/codepage.js +2 -1
- package/dist/vba/dir-records.d.ts +6 -0
- package/dist/vba/dir-records.js +10 -2
- package/dist/vba/errors.d.ts +11 -4
- package/dist/vba/index.d.ts +0 -2
- package/dist/vba/index.js +0 -2
- package/dist/vba/ms-ovba.d.ts +6 -2
- package/dist/vba/ms-ovba.js +66 -22
- package/dist/vba/project-editor.js +129 -119
- package/dist/vba/project.d.ts +15 -1
- package/dist/vba/project.js +38 -13
- package/dist/vba/vba-encoding.d.ts +4 -10
- package/dist/vba/vba-encoding.js +3 -17
- package/dist/xml/xml-attrs.d.ts +37 -0
- package/dist/xml/xml-attrs.js +30 -0
- package/dist/xml/xml-chars.d.ts +40 -0
- package/dist/xml/xml-chars.js +12 -0
- package/dist/xml/xml-namespaces.d.ts +31 -0
- package/dist/xml/xml-namespaces.js +50 -0
- package/dist/xml/xml-read.d.ts +101 -11
- package/dist/xml/xml-read.js +150 -18
- package/dist/xml/xml-scan.d.ts +25 -34
- package/dist/xml/xml-scan.js +69 -46
- package/dist/xml/xml.d.ts +59 -26
- package/dist/xml/xml.js +45 -23
- package/package.json +15 -9
- package/skills/ts-xlsx-upstream/SKILL.md +21 -13
- package/dist/core/modern-functions.d.ts +0 -1
package/dist/io/opc/inflate.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Unzip, UnzipInflate } from 'fflate';
|
|
2
2
|
import { concat } from '../../bytes.js';
|
|
3
|
+
import { quoted } from '../../errors.js';
|
|
3
4
|
import { PackageReadError } from './errors.js';
|
|
4
5
|
const INPUT_SLICE = 1 << 14;
|
|
5
6
|
export function inflatePackage(data, cap) {
|
|
@@ -7,6 +8,12 @@ export function inflatePackage(data, cap) {
|
|
|
7
8
|
let total = 0;
|
|
8
9
|
let failure;
|
|
9
10
|
const unzip = new Unzip((file) => {
|
|
11
|
+
if (!isPartName(file.name)) {
|
|
12
|
+
failure ??= new PackageReadError(`illegal part name ${quoted(file.name)} in package`);
|
|
13
|
+
}
|
|
14
|
+
else if (file.name in files) {
|
|
15
|
+
failure ??= new PackageReadError(`duplicate part ${quoted(file.name)} in package`);
|
|
16
|
+
}
|
|
10
17
|
const chunks = [];
|
|
11
18
|
let size = 0;
|
|
12
19
|
file.ondata = (error, chunk, final) => {
|
|
@@ -44,3 +51,10 @@ export function inflatePackage(data, cap) {
|
|
|
44
51
|
throw failure;
|
|
45
52
|
return files;
|
|
46
53
|
}
|
|
54
|
+
function isPartName(name) {
|
|
55
|
+
if (name === '' || name.startsWith('/') || name.includes('\\'))
|
|
56
|
+
return false;
|
|
57
|
+
if (/^[A-Za-z]:/.test(name))
|
|
58
|
+
return false;
|
|
59
|
+
return !name.split('/').includes('..');
|
|
60
|
+
}
|
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
* The SpreadsheetML vocabulary (the main namespace, DrawingML, the Microsoft extension URIs) is a
|
|
7
7
|
* property of the XML codec and lives in `../xlsx/namespaces.ts`.
|
|
8
8
|
*/
|
|
9
|
+
import type { NamespaceScope } from '../../xml/xml-namespaces.ts';
|
|
10
|
+
import { type XmlAttributes } from '../../xml/xml-scan.ts';
|
|
9
11
|
/** The `xmlns` of every `.rels` package relationships part. */
|
|
10
12
|
export declare const PKG_RELS_NS = "http://schemas.openxmlformats.org/package/2006/relationships";
|
|
11
13
|
/** The `xmlns` of the package's `[Content_Types].xml`. */
|
|
@@ -16,3 +18,17 @@ export declare const CONTENT_TYPES_NS = "http://schemas.openxmlformats.org/packa
|
|
|
16
18
|
* the relationship graph is written in, so it belongs with the graph and not with either codec.
|
|
17
19
|
*/
|
|
18
20
|
export declare const RELATIONSHIPS_NS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
|
|
21
|
+
/**
|
|
22
|
+
* A relationship attribute (`r:id`, `r:embed`) resolved by namespace rather than by prefix.
|
|
23
|
+
*
|
|
24
|
+
* A workbook may bind {@link RELATIONSHIPS_NS} to any prefix it likes, and real toolchains do: reading
|
|
25
|
+
* the literal `attrs['r:id']` lost every sheet relationship in such a file, so the sheets loaded
|
|
26
|
+
* permanently empty with no error.
|
|
27
|
+
*
|
|
28
|
+
* Falls back to the conventional `r:` spelling when the prefix is not bound anywhere in scope. That is
|
|
29
|
+
* not laxity for its own sake: a part carrying `r:embed` without declaring `r` is malformed XML that
|
|
30
|
+
* this scanner does not reject, it was read before, and refusing it now would lose a file to a fix
|
|
31
|
+
* meant to gain files. Resolution by namespace comes first, so a file that binds `r:` to something
|
|
32
|
+
* else is read correctly rather than by its prefix.
|
|
33
|
+
*/
|
|
34
|
+
export declare function relAttr(scope: NamespaceScope, attrs: XmlAttributes, local: string): string | undefined;
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import {} from '../../xml/xml-scan.js';
|
|
1
2
|
export const PKG_RELS_NS = 'http://schemas.openxmlformats.org/package/2006/relationships';
|
|
2
3
|
export const CONTENT_TYPES_NS = 'http://schemas.openxmlformats.org/package/2006/content-types';
|
|
3
4
|
export const RELATIONSHIPS_NS = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships';
|
|
5
|
+
export function relAttr(scope, attrs, local) {
|
|
6
|
+
return scope.attr(attrs, RELATIONSHIPS_NS, local) ?? attrs[`r:${local}`];
|
|
7
|
+
}
|
|
@@ -1,4 +1,13 @@
|
|
|
1
1
|
export declare const THEME_PART_PATH = "xl/theme/theme1.xml";
|
|
2
|
+
/**
|
|
3
|
+
* The extension of a part path (`xl/media/image1.JPEG` → `jpeg`), or `''` when it carries none.
|
|
4
|
+
*
|
|
5
|
+
* Lower-cased here rather than by each caller. OPC extensions are case-insensitive, so an extension is
|
|
6
|
+
* only ever used as a key or compared against a literal, and every caller but one folded the case
|
|
7
|
+
* itself; the one that did not handed a raw `JPEG` to `addImage`, which folds later, so the same media
|
|
8
|
+
* type could reach `[Content_Types].xml` as two `<Default Extension>` entries. Folding once at the
|
|
9
|
+
* source is what makes the missed fold unrepresentable rather than merely unlikely.
|
|
10
|
+
*/
|
|
2
11
|
export declare function extensionOf(partPath: string): string;
|
|
3
12
|
export declare function relsPathFor(partPath: string): string;
|
|
4
13
|
export declare function relativePartPath(fromPath: string, toPath: string): string;
|
|
@@ -2,7 +2,7 @@ export const THEME_PART_PATH = 'xl/theme/theme1.xml';
|
|
|
2
2
|
export function extensionOf(partPath) {
|
|
3
3
|
const dot = partPath.lastIndexOf('.');
|
|
4
4
|
const slash = partPath.lastIndexOf('/');
|
|
5
|
-
return dot > slash ? partPath.slice(dot + 1) : '';
|
|
5
|
+
return dot > slash ? partPath.slice(dot + 1).toLowerCase() : '';
|
|
6
6
|
}
|
|
7
7
|
export function relsPathFor(partPath) {
|
|
8
8
|
const slash = partPath.lastIndexOf('/');
|
|
@@ -23,11 +23,11 @@ export function relativePartPath(fromPath, toPath) {
|
|
|
23
23
|
return [...Array(up).fill('..'), ...toSegments.slice(common)].join('/');
|
|
24
24
|
}
|
|
25
25
|
export function resolveRelativePart(basePart, target) {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
const joined = target.startsWith('/')
|
|
27
|
+
? target
|
|
28
|
+
: `${basePart.slice(0, basePart.lastIndexOf('/') + 1)}${target}`;
|
|
29
29
|
const out = [];
|
|
30
|
-
for (const segment of
|
|
30
|
+
for (const segment of joined.split('/')) {
|
|
31
31
|
if (segment === '' || segment === '.')
|
|
32
32
|
continue;
|
|
33
33
|
if (segment === '..')
|
|
@@ -5,23 +5,36 @@ export interface PackageAccessors {
|
|
|
5
5
|
partText: (path: string) => string | undefined;
|
|
6
6
|
/** A part's raw bytes, or undefined when the package holds no such part. */
|
|
7
7
|
partBytes: (path: string) => Uint8Array | undefined;
|
|
8
|
+
/** The package's own spelling of the part the other two would answer for `path`, or undefined when
|
|
9
|
+
* there is none: the key an editor replaces or deletes, so it acts on the entry a read found rather
|
|
10
|
+
* than adding a second one beside it. */
|
|
11
|
+
partKey: (path: string) => string | undefined;
|
|
8
12
|
}
|
|
9
13
|
/** A spreadsheet package opened for reading: inflated under the read bound, its parts bound to
|
|
10
|
-
* accessors,
|
|
14
|
+
* accessors, its office document located through the package's own relationship graph, and that
|
|
15
|
+
* document read if it is XML. */
|
|
11
16
|
export interface OpenedSpreadsheet {
|
|
12
17
|
/** The inflated parts, for a reader that hands the whole package on to another codec. */
|
|
13
18
|
readonly files: Record<string, Uint8Array>;
|
|
14
19
|
readonly pkg: PackageAccessors;
|
|
15
|
-
/**
|
|
16
|
-
*
|
|
17
|
-
*
|
|
20
|
+
/**
|
|
21
|
+
* Where this package keeps its office document, resolved through `_rels/.rels`. Conventionally
|
|
22
|
+
* `xl/workbook.xml` (or `xl/workbook.bin` for a `.xlsb`), but the convention is Excel's habit
|
|
23
|
+
* rather than the format's rule, and a package is free to name the part anything its root
|
|
24
|
+
* relationship points at.
|
|
25
|
+
*/
|
|
26
|
+
readonly documentPath: string;
|
|
27
|
+
/** The office document's text, or undefined when the package's document is not XML (a `.xlsb`
|
|
28
|
+
* carries a BIFF12 part instead). Each entry point answers that case for itself: they genuinely
|
|
29
|
+
* want different answers, and the difference is documented where they diverge. */
|
|
18
30
|
readonly workbookXml: string | undefined;
|
|
19
31
|
}
|
|
20
32
|
/**
|
|
21
|
-
* Open a spreadsheet package: the six-step preamble every reader shares, and in particular the
|
|
33
|
+
* Open a spreadsheet package: the six-step preamble every reader shares, and in particular the three
|
|
22
34
|
* decisions worth having exactly one of. The inflate bound is a security decision (an unbounded
|
|
23
|
-
* inflate is a zip bomb)
|
|
24
|
-
* decision between the two codecs
|
|
35
|
+
* inflate is a zip bomb), *where the office document lives* is a question only the package can
|
|
36
|
+
* answer, and "is that document XML" is the dispatch decision between the two codecs. None should be
|
|
37
|
+
* restated once per entry point.
|
|
25
38
|
*
|
|
26
39
|
* It deliberately stops short of the shared strings and the style table: those are codec-specific
|
|
27
40
|
* (`xl/sharedStrings.xml` against BIFF12's own record stream), and lifting them here would put
|
|
@@ -29,8 +42,6 @@ export interface OpenedSpreadsheet {
|
|
|
29
42
|
*/
|
|
30
43
|
export declare function openSpreadsheetPackage(data: Uint8Array, maxUncompressedBytes: number | undefined): OpenedSpreadsheet;
|
|
31
44
|
export declare function packageAccessors(files: Record<string, Uint8Array>): PackageAccessors;
|
|
32
|
-
export declare function relationshipTargetByType(xml: string, suffix: string): string | undefined;
|
|
33
|
-
export declare function relationshipTargetsByType(xml: string, suffix: string): string[];
|
|
34
45
|
export interface RelationshipRecord {
|
|
35
46
|
readonly id: string;
|
|
36
47
|
readonly type: string;
|
|
@@ -46,14 +57,47 @@ export interface PartRelationships {
|
|
|
46
57
|
byId(id: string): RelationshipRecord | undefined;
|
|
47
58
|
/** Resolve one of this part's targets against the part's own directory. */
|
|
48
59
|
pathOf(target: string): string;
|
|
49
|
-
/** The package part reached through the first relationship
|
|
60
|
+
/** The package part reached through the first relationship of this class ({@link isRelType}), or
|
|
50
61
|
* undefined when the part declares none: the single-part lookup (notes, printer settings, drawing,
|
|
51
62
|
* background) in one call. */
|
|
52
|
-
targetPath(
|
|
53
|
-
/** Every package part reached through a relationship of this
|
|
63
|
+
targetPath(name: string): string | undefined;
|
|
64
|
+
/** Every package part reached through a relationship of this class, in declaration order. For a part
|
|
54
65
|
* class one sheet may reference more than once (tables, pivot tables). */
|
|
55
|
-
targetPaths(
|
|
66
|
+
targetPaths(name: string): string[];
|
|
67
|
+
/**
|
|
68
|
+
* The text of the single part reached through a relationship of this type, or `undefined` when the
|
|
69
|
+
* part declares no such relationship *or* names one the package does not contain.
|
|
70
|
+
*
|
|
71
|
+
* Those two are deliberately one answer. A relationship pointing at an absent part is a damaged
|
|
72
|
+
* package, and every reader here answers a damaged package the same way Excel does: the feature is
|
|
73
|
+
* simply not there, rather than the load failing over it. Collapsing `targetPath` then `partText`
|
|
74
|
+
* into one call is what lets that contract be stated once instead of at each of the seven places
|
|
75
|
+
* that used to spell it out.
|
|
76
|
+
*/
|
|
77
|
+
relatedText(name: string): string | undefined;
|
|
78
|
+
/** As {@link relatedText}, for a part whose content is opaque bytes (a printer-settings blob, an
|
|
79
|
+
* image) rather than XML. */
|
|
80
|
+
relatedBytes(name: string): Uint8Array | undefined;
|
|
81
|
+
/**
|
|
82
|
+
* The text of a part reached through the relationship that names it, falling back to
|
|
83
|
+
* `conventionalPath` only when the part declares no such relationship, and to `''` when neither
|
|
84
|
+
* resolves.
|
|
85
|
+
*
|
|
86
|
+
* The order is the whole point, and it decides more than where a lookup lands. A package is free
|
|
87
|
+
* to name any part anything -- the workbook part included -- so the part's own relationships are
|
|
88
|
+
* what say where its pool and its stylesheet live. Resolved conventional-path-first, a renamed
|
|
89
|
+
* pool reads as no pooled strings at all, and a renamed stylesheet silently changes cell *types*,
|
|
90
|
+
* because the date test reads `numFmt` off the resolved style to tell `45000` from a date. The
|
|
91
|
+
* conventional path stays as the fallback for a package whose rels are damaged.
|
|
92
|
+
*
|
|
93
|
+
* The `''` is a real package state, not a lookup that failed quietly: a workbook may carry no pool
|
|
94
|
+
* and no stylesheet, and every reader of these parses the empty text into the empty table it would
|
|
95
|
+
* have built anyway. Stated here so the two readers that resolve these parts cannot answer the
|
|
96
|
+
* same package differently -- a streamed read must not decode a cell differently from a buffered
|
|
97
|
+
* one.
|
|
98
|
+
*/
|
|
99
|
+
relatedTextOrPath(name: string, conventionalPath: string): string;
|
|
56
100
|
}
|
|
57
|
-
export declare function readPartRelationships(partPath: string, partText: (path: string) => string | undefined): PartRelationships;
|
|
101
|
+
export declare function readPartRelationships(partPath: string, partText: (path: string) => string | undefined, partBytes?: (path: string) => Uint8Array | undefined): PartRelationships;
|
|
58
102
|
export declare function contentTypeResolver(contentTypesXml: string): (path: string) => string;
|
|
59
103
|
export declare function capturePartClosure(entryPath: string, partText: (path: string) => string | undefined, partBytes: (path: string) => Uint8Array | undefined, contentTypeOf: (path: string) => string): readonly PreservedPart[] | undefined;
|
package/dist/io/opc/read-opc.js
CHANGED
|
@@ -1,37 +1,52 @@
|
|
|
1
1
|
import { strFromU8 } from 'fflate';
|
|
2
|
+
import { isRelType } from '../../rel-type.js';
|
|
2
3
|
import { openElements } from '../../xml/xml-read.js';
|
|
3
4
|
import { extensionOf, relsPathFor, resolveRelativePart } from './part-paths.js';
|
|
4
5
|
import { DEFAULT_MAX_UNCOMPRESSED } from './read-options.js';
|
|
5
6
|
import { inflateSpreadsheetPackage } from './sniff-format.js';
|
|
7
|
+
import { transitionalRelationshipType } from './strict-relationships.js';
|
|
6
8
|
export function openSpreadsheetPackage(data, maxUncompressedBytes) {
|
|
7
9
|
const files = inflateSpreadsheetPackage(data, maxUncompressedBytes ?? DEFAULT_MAX_UNCOMPRESSED);
|
|
8
10
|
const pkg = packageAccessors(files);
|
|
9
|
-
|
|
11
|
+
const documentPath = readPartRelationships('', pkg.partText).targetPath(OFFICE_DOCUMENT_REL) ??
|
|
12
|
+
conventionalDocument(pkg);
|
|
13
|
+
const workbookXml = extensionOf(documentPath) === 'xml' ? pkg.partText(documentPath) : undefined;
|
|
14
|
+
return { files, pkg, documentPath, workbookXml };
|
|
15
|
+
}
|
|
16
|
+
const OFFICE_DOCUMENT_REL = 'officeDocument';
|
|
17
|
+
const CONVENTIONAL_DOCUMENTS = ['xl/workbook.xml', 'xl/workbook.bin'];
|
|
18
|
+
function conventionalDocument(pkg) {
|
|
19
|
+
return (CONVENTIONAL_DOCUMENTS.find((path) => pkg.partBytes(path) !== undefined) ??
|
|
20
|
+
CONVENTIONAL_DOCUMENTS[0]);
|
|
10
21
|
}
|
|
11
22
|
export function packageAccessors(files) {
|
|
23
|
+
const folded = new Map();
|
|
24
|
+
for (const path of Object.keys(files)) {
|
|
25
|
+
const key = path.toLowerCase();
|
|
26
|
+
if (!folded.has(key))
|
|
27
|
+
folded.set(key, path);
|
|
28
|
+
}
|
|
29
|
+
const partKey = (path) => files[path] === undefined ? folded.get(path.toLowerCase()) : path;
|
|
30
|
+
const lookup = (path) => {
|
|
31
|
+
const key = partKey(path);
|
|
32
|
+
return key === undefined ? undefined : files[key];
|
|
33
|
+
};
|
|
12
34
|
return {
|
|
13
35
|
partText: (path) => {
|
|
14
|
-
const bytes =
|
|
36
|
+
const bytes = lookup(path);
|
|
15
37
|
return bytes === undefined ? undefined : strFromU8(bytes);
|
|
16
38
|
},
|
|
17
|
-
partBytes:
|
|
39
|
+
partBytes: lookup,
|
|
40
|
+
partKey,
|
|
18
41
|
};
|
|
19
42
|
}
|
|
20
|
-
export function relationshipTargetByType(xml, suffix) {
|
|
21
|
-
return relationshipTargetsByType(xml, suffix)[0];
|
|
22
|
-
}
|
|
23
|
-
export function relationshipTargetsByType(xml, suffix) {
|
|
24
|
-
return parseRelationshipRecords(xml)
|
|
25
|
-
.filter((record) => record.type.endsWith(`/${suffix}`))
|
|
26
|
-
.map((record) => record.target);
|
|
27
|
-
}
|
|
28
43
|
export function parseRelationshipRecords(xml) {
|
|
29
44
|
const records = [];
|
|
30
45
|
for (const { attrs } of openElements(xml, 'Relationship')) {
|
|
31
46
|
if (attrs.Id !== undefined && attrs.Type !== undefined && attrs.Target !== undefined) {
|
|
32
47
|
records.push({
|
|
33
48
|
id: attrs.Id,
|
|
34
|
-
type: attrs.Type,
|
|
49
|
+
type: transitionalRelationshipType(attrs.Type),
|
|
35
50
|
target: attrs.Target,
|
|
36
51
|
external: attrs.TargetMode === 'External',
|
|
37
52
|
});
|
|
@@ -39,17 +54,30 @@ export function parseRelationshipRecords(xml) {
|
|
|
39
54
|
}
|
|
40
55
|
return records;
|
|
41
56
|
}
|
|
42
|
-
export function readPartRelationships(partPath, partText) {
|
|
57
|
+
export function readPartRelationships(partPath, partText, partBytes) {
|
|
43
58
|
const records = parseRelationshipRecords(partText(relsPathFor(partPath)) ?? '');
|
|
44
59
|
const byId = new Map(records.map((record) => [record.id, record]));
|
|
45
60
|
const pathOf = (target) => resolveRelativePart(partPath, target);
|
|
46
|
-
const targetsOf = (
|
|
61
|
+
const targetsOf = (name) => records.filter((record) => isRelType(record.type, name)).map((record) => record.target);
|
|
62
|
+
const targetPath = (name) => targetsOf(name).map(pathOf)[0];
|
|
47
63
|
return {
|
|
48
64
|
records,
|
|
49
65
|
byId: (id) => byId.get(id),
|
|
50
66
|
pathOf,
|
|
51
|
-
targetPath
|
|
52
|
-
targetPaths: (
|
|
67
|
+
targetPath,
|
|
68
|
+
targetPaths: (name) => targetsOf(name).map(pathOf),
|
|
69
|
+
relatedText: (name) => {
|
|
70
|
+
const path = targetPath(name);
|
|
71
|
+
return path === undefined ? undefined : partText(path);
|
|
72
|
+
},
|
|
73
|
+
relatedBytes: (name) => {
|
|
74
|
+
const path = targetPath(name);
|
|
75
|
+
return path === undefined ? undefined : partBytes?.(path);
|
|
76
|
+
},
|
|
77
|
+
relatedTextOrPath: (name, conventionalPath) => {
|
|
78
|
+
const path = targetPath(name);
|
|
79
|
+
return (path === undefined ? undefined : partText(path)) ?? partText(conventionalPath) ?? '';
|
|
80
|
+
},
|
|
53
81
|
};
|
|
54
82
|
}
|
|
55
83
|
export function contentTypeResolver(contentTypesXml) {
|
|
@@ -57,7 +85,7 @@ export function contentTypeResolver(contentTypesXml) {
|
|
|
57
85
|
const defaults = new Map();
|
|
58
86
|
for (const { local, attrs } of openElements(contentTypesXml, 'Override', 'Default')) {
|
|
59
87
|
if (local === 'Override' && attrs.PartName !== undefined && attrs.ContentType !== undefined) {
|
|
60
|
-
overrides.set(attrs.PartName, attrs.ContentType);
|
|
88
|
+
overrides.set(attrs.PartName.toLowerCase(), attrs.ContentType);
|
|
61
89
|
}
|
|
62
90
|
else if (local === 'Default' &&
|
|
63
91
|
attrs.Extension !== undefined &&
|
|
@@ -65,16 +93,16 @@ export function contentTypeResolver(contentTypesXml) {
|
|
|
65
93
|
defaults.set(attrs.Extension.toLowerCase(), attrs.ContentType);
|
|
66
94
|
}
|
|
67
95
|
}
|
|
68
|
-
return (path) => overrides.get(`/${path}
|
|
69
|
-
defaults.get(extensionOf(path)
|
|
96
|
+
return (path) => overrides.get(`/${path}`.toLowerCase()) ??
|
|
97
|
+
defaults.get(extensionOf(path)) ??
|
|
70
98
|
'application/octet-stream';
|
|
71
99
|
}
|
|
72
100
|
export function capturePartClosure(entryPath, partText, partBytes, contentTypeOf) {
|
|
73
101
|
const parts = [];
|
|
74
102
|
const visited = new Set();
|
|
75
103
|
const queue = [entryPath];
|
|
76
|
-
|
|
77
|
-
const path = queue
|
|
104
|
+
for (let next = 0; next < queue.length; next++) {
|
|
105
|
+
const path = queue[next];
|
|
78
106
|
if (path === undefined || visited.has(path))
|
|
79
107
|
continue;
|
|
80
108
|
visited.add(path);
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* How a reader is allowed to open an OPC package. One knob today: the ceiling on what inflating it
|
|
3
|
+
* may produce, which every reader shares because every reader inflates the same container.
|
|
4
|
+
*/
|
|
5
|
+
export interface ReadPackageOptions {
|
|
2
6
|
/**
|
|
3
7
|
* Maximum total uncompressed output, in bytes, produced while inflating the package.
|
|
4
8
|
* The bound is enforced by a running counter as bytes are decompressed, never read from
|
|
@@ -17,11 +17,11 @@ export declare function sniffContainer(data: Uint8Array): Container;
|
|
|
17
17
|
*/
|
|
18
18
|
export declare function inflateSpreadsheetPackage(data: Uint8Array, cap: number): Record<string, Uint8Array>;
|
|
19
19
|
/**
|
|
20
|
-
* The typed error for an inflated package
|
|
21
|
-
*
|
|
20
|
+
* The typed error for an inflated package whose office document is not XML: a `.xlsb` if that
|
|
21
|
+
* document is present as binary, otherwise an unrecognised (non-workbook) ZIP.
|
|
22
22
|
*
|
|
23
23
|
* The `.xlsb` branch takes the caller's own explanation, because whether a binary workbook is
|
|
24
24
|
* readable now depends on *which* entry point was asked: `readXlsx` reads one, the row streamer
|
|
25
25
|
* cannot yet. A single baked-in "not supported" message would be wrong for one of them.
|
|
26
26
|
*/
|
|
27
|
-
export declare function unsupportedWorkbookPart(
|
|
27
|
+
export declare function unsupportedWorkbookPart(partBytes: (path: string) => Uint8Array | undefined, documentPath: string, xlsbMessage: string): UnsupportedFormatError;
|
|
@@ -34,8 +34,8 @@ export function inflateSpreadsheetPackage(data, cap) {
|
|
|
34
34
|
throw new PackageReadError('not a readable .xlsx package: the ZIP container is corrupt or truncated and could not be inflated');
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
|
-
export function unsupportedWorkbookPart(
|
|
38
|
-
if (
|
|
37
|
+
export function unsupportedWorkbookPart(partBytes, documentPath, xlsbMessage) {
|
|
38
|
+
if (partBytes(documentPath) !== undefined) {
|
|
39
39
|
return new UnsupportedFormatError('xlsb', xlsbMessage);
|
|
40
40
|
}
|
|
41
41
|
return new UnsupportedFormatError('unknown');
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
const STRICT_RELATIONSHIPS = 'http://purl.oclc.org/ooxml/officeDocument/relationships/';
|
|
2
|
+
const TRANSITIONAL_RELATIONSHIPS = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/';
|
|
3
|
+
const RENAMED = new Map([
|
|
4
|
+
['extendedProperties', 'extended-properties'],
|
|
5
|
+
['customProperties', 'custom-properties'],
|
|
6
|
+
]);
|
|
7
|
+
export function transitionalRelationshipType(type) {
|
|
8
|
+
if (!type.startsWith(STRICT_RELATIONSHIPS))
|
|
9
|
+
return type;
|
|
10
|
+
const name = type.slice(STRICT_RELATIONSHIPS.length);
|
|
11
|
+
return `${TRANSITIONAL_RELATIONSHIPS}${RENAMED.get(name) ?? name}`;
|
|
12
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A per-sheet allowance of column touches, spent by each column run the reader applies.
|
|
3
|
+
*
|
|
4
|
+
* A run past the allowance is truncated, and one starting past it is dropped, rather than the read
|
|
5
|
+
* being refused: that is the stance every reader already takes on an out-of-grid run, and it means a
|
|
6
|
+
* hostile file loses formatting it could not have meant while a real one is untouched.
|
|
7
|
+
*/
|
|
8
|
+
export declare class ColumnRecordBudget {
|
|
9
|
+
#private;
|
|
10
|
+
/** The last column of `[first, last]` this sheet can still afford, or `undefined` when none is. */
|
|
11
|
+
take(first: number, last: number): number | undefined;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* The columns of a declared run `[first, last]`, one-based, that a sheet actually applies, or
|
|
15
|
+
* `undefined` for a run that applies to none: one starting past the grid, an empty one, or one the
|
|
16
|
+
* budget can no longer afford.
|
|
17
|
+
*
|
|
18
|
+
* Beside the budget because the two decisions are one decision. The XML readers each had their own
|
|
19
|
+
* copy, and the copies had already drifted: one tested `first > MAX_COLUMN` and the other did not,
|
|
20
|
+
* which happened not to matter only because `budget.take` returns `undefined` when `first > last`, an
|
|
21
|
+
* accident of that method's contract rather than an agreement between the readers.
|
|
22
|
+
*/
|
|
23
|
+
export declare function clampColumnSpan(first: number, last: number, budget: ColumnRecordBudget): {
|
|
24
|
+
first: number;
|
|
25
|
+
last: number;
|
|
26
|
+
} | undefined;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { MAX_COLUMN } from '../../core/address.js';
|
|
2
|
+
const MAX_COLUMN_RECORD_TOUCHES = MAX_COLUMN * 4;
|
|
3
|
+
export class ColumnRecordBudget {
|
|
4
|
+
#remaining = MAX_COLUMN_RECORD_TOUCHES;
|
|
5
|
+
take(first, last) {
|
|
6
|
+
if (this.#remaining <= 0 || first > last)
|
|
7
|
+
return undefined;
|
|
8
|
+
const affordable = Math.min(last, first + this.#remaining - 1);
|
|
9
|
+
this.#remaining -= affordable - first + 1;
|
|
10
|
+
return affordable;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export function clampColumnSpan(first, last, budget) {
|
|
14
|
+
if (first > MAX_COLUMN)
|
|
15
|
+
return undefined;
|
|
16
|
+
const affordable = budget.take(first, Math.min(last, MAX_COLUMN));
|
|
17
|
+
return affordable === undefined ? undefined : { first, last: affordable };
|
|
18
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { type FormulaPlacement } from '../../core/array-formula-ranges.ts';
|
|
2
|
+
import type { Worksheet } from '../../core/worksheet.ts';
|
|
3
|
+
/**
|
|
4
|
+
* A sheet name from a file, rewritten into one the model will accept.
|
|
5
|
+
*
|
|
6
|
+
* Excel's own repair, in the order the constraints interact: forbidden characters go first (they
|
|
7
|
+
* can be anywhere), then the length ceiling, then the apostrophe rule *again*, because truncating
|
|
8
|
+
* can expose an interior apostrophe at the new edge. An empty result becomes `Sheet{n}`, and a
|
|
9
|
+
* collision takes a ` (2)` suffix that is itself made to fit inside the 31-character limit.
|
|
10
|
+
*
|
|
11
|
+
* @param taken the lower-cased names already in the workbook; sheet names collide
|
|
12
|
+
* case-insensitively, which is the comparison `Workbook.getWorksheet` makes.
|
|
13
|
+
*/
|
|
14
|
+
export declare function repairSheetName(name: string, taken: ReadonlySet<string>): string;
|
|
15
|
+
/**
|
|
16
|
+
* A workbook's declared sheets in order, each carrying the name the model will accept: repaired by
|
|
17
|
+
* {@link repairSheetName} against every name repaired before it.
|
|
18
|
+
*
|
|
19
|
+
* Both codecs declare their sheets as an ordered list and both have to know the repaired names before
|
|
20
|
+
* anything cites a sheet by position, since a scoped defined name and a 3-D reference both index into
|
|
21
|
+
* that order. The loop threading the taken set through the list is one loop, so it is written once.
|
|
22
|
+
*/
|
|
23
|
+
export declare function repairedSheetNames<T extends {
|
|
24
|
+
readonly name: string;
|
|
25
|
+
}>(declared: readonly T[]): T[];
|
|
26
|
+
/**
|
|
27
|
+
* Read a sheet whose array formulas cannot stand as Excel repairs it (`core/array-formula-ranges.ts`). A
|
|
28
|
+
* file is free to carry an array formula whose range holds another formula, or shares a cell with another
|
|
29
|
+
* array formula's range, and the writer refuses both, since Excel offers to repair such a package.
|
|
30
|
+
*
|
|
31
|
+
* Excel 16.0 keeps the array formula decided first. A formula inside its range loses its formula and its
|
|
32
|
+
* value, keeping its style, and a shared formula whose master that was loses its formula too, keeping its
|
|
33
|
+
* value; an array formula whose range only shares cells with it loses its formula, keeping its value. The
|
|
34
|
+
* value Excel then shows in a cell it emptied is what the kept array formula computes there, which the
|
|
35
|
+
* library does not compute, so the cell is left empty.
|
|
36
|
+
*/
|
|
37
|
+
export declare function admitArrayRanges(sheet: Worksheet): void;
|
|
38
|
+
/** What Excel's repair takes from a sheet whose array formulas cannot stand. */
|
|
39
|
+
export interface ArrayRangeRepair {
|
|
40
|
+
/** Formulas whose cell lies in the range of an array formula kept: each loses its whole cell. */
|
|
41
|
+
readonly cellsRemoved: FormulaPlacement[];
|
|
42
|
+
/**
|
|
43
|
+
* Array formulas whose own cell lies in no range kept but whose range shares a cell with one: each loses
|
|
44
|
+
* its formula and keeps the value it cached.
|
|
45
|
+
*/
|
|
46
|
+
readonly formulasRemoved: FormulaPlacement[];
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* What Excel's repair takes from the sheet, deciding in reading order: an array formula keeps its range
|
|
50
|
+
* unless that range shares a cell with a range kept before it. A formula of any kind whose cell lies in a
|
|
51
|
+
* kept range loses the cell; an array formula whose range shares a cell with a kept range, its own cell
|
|
52
|
+
* outside it, loses its formula and keeps its value. What is left holds no conflict.
|
|
53
|
+
*
|
|
54
|
+
* Excel 16.0 (build 20326) repaired B1:B3 holding a plain formula, or an array formula of its own, in B2
|
|
55
|
+
* by keeping B1's range and removing B2's "cell information", and B1:C2 beside A2:B3 by keeping B1's range
|
|
56
|
+
* and removing A2's "formula", its value left (`test/corpus/fixtures/excel-oracle/array-formula-ranges.json`).
|
|
57
|
+
* An array formula's own cell is the first of its range in reading order, so a range is always decided
|
|
58
|
+
* before any cell it holds, and a range whose formula was taken claims none.
|
|
59
|
+
*/
|
|
60
|
+
export declare function arrayRangeRepair(placements: Iterable<FormulaPlacement>): ArrayRangeRepair;
|
|
61
|
+
/**
|
|
62
|
+
* Run a model call that is being handed foreign input, and answer `undefined` where the model
|
|
63
|
+
* refuses it.
|
|
64
|
+
*
|
|
65
|
+
* The three caught types are the three a model method raises *about its argument*: an
|
|
66
|
+
* `AuthoringError` for a composite that cannot exist, and the native `RangeError` / `SyntaxError`
|
|
67
|
+
* the taxonomy assigns to a single scalar that is out of range or does not parse. Every one of them
|
|
68
|
+
* is a statement about the caller, and on this path the caller is a file.
|
|
69
|
+
*
|
|
70
|
+
* Nothing else is caught. An `XlsxError` raised by a layer below is a real failure of the read and
|
|
71
|
+
* keeps its identity; an `InternalError` is a bug of ours and must not be swallowed by a reader
|
|
72
|
+
* being tolerant about somebody else's file.
|
|
73
|
+
*/
|
|
74
|
+
export declare function admitting<T>(build: () => T): T | undefined;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { tryDecodeAnchoredRange } from '../../core/address.js';
|
|
2
|
+
import { formulaPlacement } from '../../core/array-formula-ranges.js';
|
|
3
|
+
import { INVALID_SHEET_NAME_CHARS, MAX_SHEET_NAME_LENGTH } from '../../core/limits.js';
|
|
4
|
+
import { MergeIndex } from '../../core/merge-index.js';
|
|
5
|
+
import { isFormulaValue, isSharedFormulaValue } from '../../core/value.js';
|
|
6
|
+
import { AuthoringError } from '../../errors.js';
|
|
7
|
+
const INVALID_SHEET_NAME_CHARS_GLOBAL = new RegExp(INVALID_SHEET_NAME_CHARS.source, 'g');
|
|
8
|
+
export function repairSheetName(name, taken) {
|
|
9
|
+
const stripped = trimApostrophes(name.replace(INVALID_SHEET_NAME_CHARS_GLOBAL, '')).slice(0, MAX_SHEET_NAME_LENGTH);
|
|
10
|
+
const base = trimApostrophes(stripped);
|
|
11
|
+
if (base.length === 0)
|
|
12
|
+
return firstFree((n) => `Sheet${n}`, taken);
|
|
13
|
+
if (!taken.has(base.toLowerCase()))
|
|
14
|
+
return base;
|
|
15
|
+
return firstFree((n) => withSuffix(base, ` (${n})`), taken, 2);
|
|
16
|
+
}
|
|
17
|
+
export function repairedSheetNames(declared) {
|
|
18
|
+
const taken = new Set();
|
|
19
|
+
return declared.map((entry) => {
|
|
20
|
+
const name = repairSheetName(entry.name, taken);
|
|
21
|
+
taken.add(name.toLowerCase());
|
|
22
|
+
return { ...entry, name };
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
function trimApostrophes(name) {
|
|
26
|
+
return name.replace(/^'+/, '').replace(/'+$/, '');
|
|
27
|
+
}
|
|
28
|
+
function firstFree(candidate, taken, from = 1) {
|
|
29
|
+
for (let n = from;; n++) {
|
|
30
|
+
const name = candidate(n);
|
|
31
|
+
if (!taken.has(name.toLowerCase()))
|
|
32
|
+
return name;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function withSuffix(base, suffix) {
|
|
36
|
+
const room = MAX_SHEET_NAME_LENGTH - suffix.length;
|
|
37
|
+
return trimApostrophes(base.slice(0, room)) + suffix;
|
|
38
|
+
}
|
|
39
|
+
export function admitArrayRanges(sheet) {
|
|
40
|
+
const placements = [];
|
|
41
|
+
for (const { cells } of sheet.rows()) {
|
|
42
|
+
for (const cell of cells) {
|
|
43
|
+
const placement = formulaPlacement(cell.address, cell.col, cell.row, cell.value);
|
|
44
|
+
if (placement !== undefined)
|
|
45
|
+
placements.push(placement);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
const { cellsRemoved, formulasRemoved } = arrayRangeRepair(placements);
|
|
49
|
+
if (cellsRemoved.length === 0 && formulasRemoved.length === 0)
|
|
50
|
+
return;
|
|
51
|
+
const mastersRemoved = new Set();
|
|
52
|
+
for (const { address } of cellsRemoved) {
|
|
53
|
+
const cell = sheet.getCell(address);
|
|
54
|
+
if (isFormulaValue(cell.value))
|
|
55
|
+
mastersRemoved.add(address);
|
|
56
|
+
cell.value = null;
|
|
57
|
+
}
|
|
58
|
+
for (const { address } of formulasRemoved) {
|
|
59
|
+
const cell = sheet.getCell(address);
|
|
60
|
+
cell.value = cachedResult(cell.value);
|
|
61
|
+
}
|
|
62
|
+
if (mastersRemoved.size === 0)
|
|
63
|
+
return;
|
|
64
|
+
for (const { cells } of sheet.rows()) {
|
|
65
|
+
for (const cell of cells) {
|
|
66
|
+
const value = cell.value;
|
|
67
|
+
if (isSharedFormulaValue(value) && mastersRemoved.has(value.sharedFormula)) {
|
|
68
|
+
cell.value = cachedResult(value);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
export function arrayRangeRepair(placements) {
|
|
74
|
+
const ordered = [...placements].sort((a, b) => a.row - b.row || a.col - b.col);
|
|
75
|
+
const keptRanges = [];
|
|
76
|
+
const kept = new MergeIndex(keptRanges);
|
|
77
|
+
const cellsRemoved = [];
|
|
78
|
+
const formulasRemoved = [];
|
|
79
|
+
for (const placement of ordered) {
|
|
80
|
+
const { col, row } = placement;
|
|
81
|
+
if (kept.overlapping({ top: row, left: col, bottom: row, right: col }) !== undefined) {
|
|
82
|
+
cellsRemoved.push(placement);
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
const range = placement.arrayRef === undefined
|
|
86
|
+
? undefined
|
|
87
|
+
: tryDecodeAnchoredRange(placement.arrayRef, col, row);
|
|
88
|
+
if (range === undefined)
|
|
89
|
+
continue;
|
|
90
|
+
if (kept.overlapping(range) !== undefined) {
|
|
91
|
+
formulasRemoved.push(placement);
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
keptRanges.push(range);
|
|
95
|
+
kept.note(range);
|
|
96
|
+
}
|
|
97
|
+
return { cellsRemoved, formulasRemoved };
|
|
98
|
+
}
|
|
99
|
+
function cachedResult(value) {
|
|
100
|
+
return typeof value === 'object' &&
|
|
101
|
+
value !== null &&
|
|
102
|
+
'result' in value &&
|
|
103
|
+
value.result !== undefined
|
|
104
|
+
? value.result
|
|
105
|
+
: null;
|
|
106
|
+
}
|
|
107
|
+
export function admitting(build) {
|
|
108
|
+
try {
|
|
109
|
+
return build();
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
if (error instanceof AuthoringError ||
|
|
113
|
+
error instanceof RangeError ||
|
|
114
|
+
error instanceof SyntaxError) {
|
|
115
|
+
return undefined;
|
|
116
|
+
}
|
|
117
|
+
throw error;
|
|
118
|
+
}
|
|
119
|
+
}
|