@shbernal/ts-xlsx 1.0.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/LICENSE +23 -0
- package/README.md +230 -0
- package/dist/core/address.d.ts +49 -0
- package/dist/core/address.js +125 -0
- package/dist/core/autofilter.d.ts +55 -0
- package/dist/core/autofilter.js +48 -0
- package/dist/core/cell.d.ts +132 -0
- package/dist/core/cell.js +249 -0
- package/dist/core/color-resolution.d.ts +58 -0
- package/dist/core/color-resolution.js +170 -0
- package/dist/core/column.d.ts +87 -0
- package/dist/core/column.js +167 -0
- package/dist/core/comment-thread.d.ts +116 -0
- package/dist/core/comment-thread.js +62 -0
- package/dist/core/conditional-formatting-overlay.d.ts +15 -0
- package/dist/core/conditional-formatting-overlay.js +24 -0
- package/dist/core/conditional-formatting.d.ts +70 -0
- package/dist/core/conditional-formatting.js +35 -0
- package/dist/core/containers.d.ts +2 -0
- package/dist/core/containers.js +25 -0
- package/dist/core/data-validation-overlay.d.ts +28 -0
- package/dist/core/data-validation-overlay.js +56 -0
- package/dist/core/data-validation.d.ts +38 -0
- package/dist/core/data-validation.js +13 -0
- package/dist/core/date.d.ts +25 -0
- package/dist/core/date.js +58 -0
- package/dist/core/formula.d.ts +49 -0
- package/dist/core/formula.js +319 -0
- package/dist/core/grid-edits.d.ts +22 -0
- package/dist/core/grid-edits.js +206 -0
- package/dist/core/image.d.ts +76 -0
- package/dist/core/image.js +65 -0
- package/dist/core/internal.d.ts +11 -0
- package/dist/core/internal.js +29 -0
- package/dist/core/merge.d.ts +13 -0
- package/dist/core/merge.js +26 -0
- package/dist/core/modern-functions.d.ts +1 -0
- package/dist/core/modern-functions.js +165 -0
- package/dist/core/page-setup.d.ts +94 -0
- package/dist/core/page-setup.js +6 -0
- package/dist/core/pivot-table.d.ts +113 -0
- package/dist/core/pivot-table.js +249 -0
- package/dist/core/preserved.d.ts +58 -0
- package/dist/core/preserved.js +5 -0
- package/dist/core/protection.d.ts +67 -0
- package/dist/core/protection.js +68 -0
- package/dist/core/range.d.ts +112 -0
- package/dist/core/range.js +283 -0
- package/dist/core/row.d.ts +73 -0
- package/dist/core/row.js +135 -0
- package/dist/core/style.d.ts +246 -0
- package/dist/core/style.js +125 -0
- package/dist/core/table-style.d.ts +67 -0
- package/dist/core/table-style.js +90 -0
- package/dist/core/table.d.ts +194 -0
- package/dist/core/table.js +291 -0
- package/dist/core/theme.d.ts +90 -0
- package/dist/core/theme.js +272 -0
- package/dist/core/value.d.ts +115 -0
- package/dist/core/value.js +144 -0
- package/dist/core/workbook-protection.d.ts +27 -0
- package/dist/core/workbook-protection.js +25 -0
- package/dist/core/workbook.d.ts +528 -0
- package/dist/core/workbook.js +732 -0
- package/dist/core/worksheet-model.d.ts +34 -0
- package/dist/core/worksheet-model.js +100 -0
- package/dist/core/worksheet.d.ts +647 -0
- package/dist/core/worksheet.js +1025 -0
- package/dist/customui/errors.d.ts +15 -0
- package/dist/customui/errors.js +15 -0
- package/dist/customui/index.d.ts +2 -0
- package/dist/customui/index.js +7 -0
- package/dist/customui/ribbon.d.ts +86 -0
- package/dist/customui/ribbon.js +180 -0
- package/dist/entries/core.d.ts +23 -0
- package/dist/entries/core.js +20 -0
- package/dist/entries/csv.d.ts +2 -0
- package/dist/entries/csv.js +6 -0
- package/dist/entries/customui.d.ts +1 -0
- package/dist/entries/customui.js +3 -0
- package/dist/entries/errors.d.ts +7 -0
- package/dist/entries/errors.js +20 -0
- package/dist/entries/vba.d.ts +2 -0
- package/dist/entries/vba.js +7 -0
- package/dist/entries/xlsb.d.ts +1 -0
- package/dist/entries/xlsb.js +7 -0
- package/dist/entries/xlsx.d.ts +5 -0
- package/dist/entries/xlsx.js +18 -0
- package/dist/errors.d.ts +65 -0
- package/dist/errors.js +66 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +19 -0
- package/dist/io/csv/read.d.ts +15 -0
- package/dist/io/csv/read.js +113 -0
- package/dist/io/csv/write.d.ts +28 -0
- package/dist/io/csv/write.js +124 -0
- package/dist/io/opc/errors.d.ts +46 -0
- package/dist/io/opc/errors.js +49 -0
- package/dist/io/opc/inflate.d.ts +15 -0
- package/dist/io/opc/inflate.js +91 -0
- package/dist/io/opc/namespaces.d.ts +18 -0
- package/dist/io/opc/namespaces.js +18 -0
- package/dist/io/opc/part-paths.d.ts +4 -0
- package/dist/io/opc/part-paths.js +35 -0
- package/dist/io/opc/read-opc.d.ts +24 -0
- package/dist/io/opc/read-opc.js +153 -0
- package/dist/io/opc/read-options.d.ts +10 -0
- package/dist/io/opc/read-options.js +7 -0
- package/dist/io/opc/rels.d.ts +15 -0
- package/dist/io/opc/rels.js +28 -0
- package/dist/io/opc/sniff-format.d.ts +27 -0
- package/dist/io/opc/sniff-format.js +92 -0
- package/dist/io/style/xf-style.d.ts +47 -0
- package/dist/io/style/xf-style.js +95 -0
- package/dist/io/xlsb/errors.d.ts +15 -0
- package/dist/io/xlsb/errors.js +15 -0
- package/dist/io/xlsb/formula.d.ts +48 -0
- package/dist/io/xlsb/formula.js +398 -0
- package/dist/io/xlsb/primitives.d.ts +86 -0
- package/dist/io/xlsb/primitives.js +230 -0
- package/dist/io/xlsb/ptg-functions.d.ts +16 -0
- package/dist/io/xlsb/ptg-functions.js +659 -0
- package/dist/io/xlsb/read-shared-strings.d.ts +2 -0
- package/dist/io/xlsb/read-shared-strings.js +22 -0
- package/dist/io/xlsb/read-styles.d.ts +4 -0
- package/dist/io/xlsb/read-styles.js +386 -0
- package/dist/io/xlsb/read-worksheet.d.ts +9 -0
- package/dist/io/xlsb/read-worksheet.js +301 -0
- package/dist/io/xlsb/read.d.ts +20 -0
- package/dist/io/xlsb/read.js +199 -0
- package/dist/io/xlsb/record-stream.d.ts +14 -0
- package/dist/io/xlsb/record-stream.js +56 -0
- package/dist/io/xlsb/record-types.d.ts +55 -0
- package/dist/io/xlsb/record-types.js +73 -0
- package/dist/io/xlsx/cell-accumulator.d.ts +25 -0
- package/dist/io/xlsx/cell-accumulator.js +172 -0
- package/dist/io/xlsx/cell-value.d.ts +32 -0
- package/dist/io/xlsx/cell-value.js +85 -0
- package/dist/io/xlsx/comments.d.ts +74 -0
- package/dist/io/xlsx/comments.js +243 -0
- package/dist/io/xlsx/conditional-formatting.d.ts +32 -0
- package/dist/io/xlsx/conditional-formatting.js +464 -0
- package/dist/io/xlsx/data-validation.d.ts +20 -0
- package/dist/io/xlsx/data-validation.js +258 -0
- package/dist/io/xlsx/edit-vba.d.ts +23 -0
- package/dist/io/xlsx/edit-vba.js +139 -0
- package/dist/io/xlsx/errors.d.ts +17 -0
- package/dist/io/xlsx/errors.js +17 -0
- package/dist/io/xlsx/hyperlinks.d.ts +42 -0
- package/dist/io/xlsx/hyperlinks.js +139 -0
- package/dist/io/xlsx/images.d.ts +34 -0
- package/dist/io/xlsx/images.js +218 -0
- package/dist/io/xlsx/namespaces.d.ts +55 -0
- package/dist/io/xlsx/namespaces.js +55 -0
- package/dist/io/xlsx/package-plan.d.ts +103 -0
- package/dist/io/xlsx/package-plan.js +168 -0
- package/dist/io/xlsx/pivot-read.d.ts +5 -0
- package/dist/io/xlsx/pivot-read.js +134 -0
- package/dist/io/xlsx/pivot.d.ts +12 -0
- package/dist/io/xlsx/pivot.js +141 -0
- package/dist/io/xlsx/read-rows.d.ts +85 -0
- package/dist/io/xlsx/read-rows.js +286 -0
- package/dist/io/xlsx/read-styles.d.ts +8 -0
- package/dist/io/xlsx/read-styles.js +551 -0
- package/dist/io/xlsx/read-worksheet.d.ts +4 -0
- package/dist/io/xlsx/read-worksheet.js +528 -0
- package/dist/io/xlsx/read.d.ts +32 -0
- package/dist/io/xlsx/read.js +680 -0
- package/dist/io/xlsx/relationships.d.ts +33 -0
- package/dist/io/xlsx/relationships.js +47 -0
- package/dist/io/xlsx/rich-runs.d.ts +13 -0
- package/dist/io/xlsx/rich-runs.js +59 -0
- package/dist/io/xlsx/rich-text.d.ts +7 -0
- package/dist/io/xlsx/rich-text.js +23 -0
- package/dist/io/xlsx/shared-formulas.d.ts +6 -0
- package/dist/io/xlsx/shared-formulas.js +52 -0
- package/dist/io/xlsx/shared-strings-read.d.ts +2 -0
- package/dist/io/xlsx/shared-strings-read.js +70 -0
- package/dist/io/xlsx/shared-strings.d.ts +17 -0
- package/dist/io/xlsx/shared-strings.js +50 -0
- package/dist/io/xlsx/sheet-properties.d.ts +13 -0
- package/dist/io/xlsx/sheet-properties.js +211 -0
- package/dist/io/xlsx/styles.d.ts +138 -0
- package/dist/io/xlsx/styles.js +821 -0
- package/dist/io/xlsx/tables.d.ts +9 -0
- package/dist/io/xlsx/tables.js +208 -0
- package/dist/io/xlsx/threaded-comments.d.ts +102 -0
- package/dist/io/xlsx/threaded-comments.js +308 -0
- package/dist/io/xlsx/workbook-xml.d.ts +12 -0
- package/dist/io/xlsx/workbook-xml.js +353 -0
- package/dist/io/xlsx/worksheet-xml.d.ts +86 -0
- package/dist/io/xlsx/worksheet-xml.js +643 -0
- package/dist/io/xlsx/write-stream.d.ts +171 -0
- package/dist/io/xlsx/write-stream.js +399 -0
- package/dist/io/xlsx/write.d.ts +81 -0
- package/dist/io/xlsx/write.js +396 -0
- package/dist/io/xlsx/x14-ext.d.ts +9 -0
- package/dist/io/xlsx/x14-ext.js +12 -0
- package/dist/vba/cfb-writer.d.ts +19 -0
- package/dist/vba/cfb-writer.js +277 -0
- package/dist/vba/cfb.d.ts +16 -0
- package/dist/vba/cfb.js +324 -0
- package/dist/vba/codepage.d.ts +18 -0
- package/dist/vba/codepage.js +87 -0
- package/dist/vba/errors.d.ts +24 -0
- package/dist/vba/errors.js +24 -0
- package/dist/vba/index.d.ts +5 -0
- package/dist/vba/index.js +15 -0
- package/dist/vba/ms-ovba.d.ts +12 -0
- package/dist/vba/ms-ovba.js +176 -0
- package/dist/vba/project-editor.d.ts +62 -0
- package/dist/vba/project-editor.js +377 -0
- package/dist/vba/project.d.ts +45 -0
- package/dist/vba/project.js +160 -0
- package/dist/vba/vba-encoding.d.ts +14 -0
- package/dist/vba/vba-encoding.js +39 -0
- package/dist/xml/errors.d.ts +15 -0
- package/dist/xml/errors.js +15 -0
- package/dist/xml/xml-read.d.ts +107 -0
- package/dist/xml/xml-read.js +283 -0
- package/dist/xml/xml.d.ts +41 -0
- package/dist/xml/xml.js +86 -0
- package/package.json +117 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// Data validation — the input constraints Excel enforces on a cell (a dropdown list, a numeric
|
|
2
|
+
// range, a length limit, a custom formula). Unlike a style facet, a validation is not owned by one
|
|
3
|
+
// cell: it is a worksheet-level overlay keyed by a target range (`sqref`), and a cell inherits
|
|
4
|
+
// whichever rule's range contains it. Keying by range is what keeps a whole-column dropdown a single
|
|
5
|
+
// entry rather than a million per-cell copies.
|
|
6
|
+
/** A defensive copy of a rule, so a stored validation never aliases the caller's object (nor its
|
|
7
|
+
* `formulae` array). */
|
|
8
|
+
export function cloneDataValidation(rule) {
|
|
9
|
+
return {
|
|
10
|
+
...rule,
|
|
11
|
+
...(rule.formulae !== undefined ? { formulae: [...rule.formulae] } : {}),
|
|
12
|
+
};
|
|
13
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The number format applied to a `Date` cell that carries no explicit format of its own,
|
|
3
|
+
* so the value renders — and reads back — as a date rather than a bare serial number.
|
|
4
|
+
*/
|
|
5
|
+
export declare const DEFAULT_DATE_NUMFMT = "yyyy-mm-dd";
|
|
6
|
+
/**
|
|
7
|
+
* Convert a JS `Date` to its 1900-system Excel serial, reproducing the phantom-leap-day
|
|
8
|
+
* quirk so the value renders on the calendar date Excel would show. Fractional serials
|
|
9
|
+
* carry the time of day. The caller must reject a non-finite (invalid) date before here.
|
|
10
|
+
*/
|
|
11
|
+
export declare function dateToSerial(date: Date): number;
|
|
12
|
+
/**
|
|
13
|
+
* Convert a 1900-system Excel serial back to a UTC `Date`, accounting for the phantom
|
|
14
|
+
* 1900-02-29 so serial 1 reads as 1900-01-01 (not 1899-12-31) and consecutive serials
|
|
15
|
+
* map to consecutive days.
|
|
16
|
+
*/
|
|
17
|
+
export declare function serialToDate(serial: number): Date;
|
|
18
|
+
/**
|
|
19
|
+
* Whether a number-format code renders its value as a date or time. A format is a date
|
|
20
|
+
* format when, once its non-formatting sections are removed — bracketed color/locale/
|
|
21
|
+
* condition directives, quoted literals, and escaped characters — any of the date/time
|
|
22
|
+
* placeholder letters (`y m d h s`) remain. So `"$"#,##0.00` and `0.00%` are not dates
|
|
23
|
+
* while `yyyy-mm-dd`, `dd/mm/yyyy`, and `[$-409]mmmm d, yyyy` are.
|
|
24
|
+
*/
|
|
25
|
+
export declare function isDateFormat(code: string): boolean;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// Excel date serials — the bridge between a JS `Date` and OOXML's numeric storage.
|
|
2
|
+
//
|
|
3
|
+
// A spreadsheet never stores a date as such: it stores a number (the "serial") and a
|
|
4
|
+
// number format that tells the viewer to render that number as a date. Serial 1 is
|
|
5
|
+
// 1900-01-01. The catch is a bug Excel has carried since 1985 and every consumer must
|
|
6
|
+
// reproduce for compatibility: the 1900 date system counts a phantom 1900-02-29 that
|
|
7
|
+
// never existed (serial 60), so serials at or below 59 sit one calendar day later than
|
|
8
|
+
// a naive "days since the epoch" offset would place them. Round-tripping a date means
|
|
9
|
+
// reproducing that quirk faithfully in both directions, or imported pre-March-1900
|
|
10
|
+
// dates land a day early.
|
|
11
|
+
//
|
|
12
|
+
// Dates here are treated as UTC wall-clock: a serial carries no timezone, so we convert
|
|
13
|
+
// against a UTC epoch in both directions. A `Date` written and read back through this
|
|
14
|
+
// module is exact; the caller owns any timezone interpretation before it reaches here.
|
|
15
|
+
const MS_PER_DAY = 86_400_000;
|
|
16
|
+
// Serial 0 in the 1900 date system is nominally 1899-12-30. The phantom leap day lives
|
|
17
|
+
// at serial 60, so the offset correction pivots there.
|
|
18
|
+
const EPOCH_1900_UTC = Date.UTC(1899, 11, 30);
|
|
19
|
+
const PHANTOM_SERIAL = 60;
|
|
20
|
+
/**
|
|
21
|
+
* The number format applied to a `Date` cell that carries no explicit format of its own,
|
|
22
|
+
* so the value renders — and reads back — as a date rather than a bare serial number.
|
|
23
|
+
*/
|
|
24
|
+
export const DEFAULT_DATE_NUMFMT = 'yyyy-mm-dd';
|
|
25
|
+
/**
|
|
26
|
+
* Convert a JS `Date` to its 1900-system Excel serial, reproducing the phantom-leap-day
|
|
27
|
+
* quirk so the value renders on the calendar date Excel would show. Fractional serials
|
|
28
|
+
* carry the time of day. The caller must reject a non-finite (invalid) date before here.
|
|
29
|
+
*/
|
|
30
|
+
export function dateToSerial(date) {
|
|
31
|
+
const days = (date.getTime() - EPOCH_1900_UTC) / MS_PER_DAY;
|
|
32
|
+
// Days at or below the phantom (1900-02-28 is day 60 from the nominal epoch) are shifted
|
|
33
|
+
// one earlier to skip the fake 1900-02-29 that Excel counts at serial 60.
|
|
34
|
+
return days <= PHANTOM_SERIAL ? days - 1 : days;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Convert a 1900-system Excel serial back to a UTC `Date`, accounting for the phantom
|
|
38
|
+
* 1900-02-29 so serial 1 reads as 1900-01-01 (not 1899-12-31) and consecutive serials
|
|
39
|
+
* map to consecutive days.
|
|
40
|
+
*/
|
|
41
|
+
export function serialToDate(serial) {
|
|
42
|
+
const dayOffset = serial < PHANTOM_SERIAL ? serial + 1 : serial;
|
|
43
|
+
return new Date(EPOCH_1900_UTC + dayOffset * MS_PER_DAY);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Whether a number-format code renders its value as a date or time. A format is a date
|
|
47
|
+
* format when, once its non-formatting sections are removed — bracketed color/locale/
|
|
48
|
+
* condition directives, quoted literals, and escaped characters — any of the date/time
|
|
49
|
+
* placeholder letters (`y m d h s`) remain. So `"$"#,##0.00` and `0.00%` are not dates
|
|
50
|
+
* while `yyyy-mm-dd`, `dd/mm/yyyy`, and `[$-409]mmmm d, yyyy` are.
|
|
51
|
+
*/
|
|
52
|
+
export function isDateFormat(code) {
|
|
53
|
+
const stripped = code
|
|
54
|
+
.replace(/\[[^\]]*\]/g, '')
|
|
55
|
+
.replace(/"[^"]*"/g, '')
|
|
56
|
+
.replace(/\\./g, '');
|
|
57
|
+
return /[ymdhs]/i.test(stripped);
|
|
58
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Quote a sheet name for use in a reference exactly when Excel would: a name that is not a plain
|
|
3
|
+
* identifier — or that would read as a cell address — is wrapped in single quotes with its internal
|
|
4
|
+
* quotes doubled, and a simple name is left bare. Shared by everything that *builds* a qualified
|
|
5
|
+
* reference: the `_FilterDatabase` name the writer derives from an autofilter, and the `.xlsb`
|
|
6
|
+
* reader's Ptg decoder, which has only a sheet index to work from and must spell the prefix itself.
|
|
7
|
+
*
|
|
8
|
+
* `last` names the far end of a 3-D span (`Data:More!A1`). A span is quoted as a whole or not at all,
|
|
9
|
+
* because the quotes delimit the sheet *reference* rather than either name — so one awkward endpoint
|
|
10
|
+
* puts both inside the quotes.
|
|
11
|
+
*/
|
|
12
|
+
export declare function quoteSheetName(name: string, last?: string): string;
|
|
13
|
+
/**
|
|
14
|
+
* Prefix every modern function called by its plain name with `_xlfn.` so Excel accepts the stored
|
|
15
|
+
* formula. Names already prefixed are left alone (never doubled), unknown/legacy functions pass
|
|
16
|
+
* through untouched, and opaque regions (string literals, sheet names, structured references) are
|
|
17
|
+
* preserved verbatim. No other rewriting occurs — in particular no `@` implicit-intersection operator
|
|
18
|
+
* is ever introduced.
|
|
19
|
+
*/
|
|
20
|
+
export declare function mangleFunctions(formula: string): string;
|
|
21
|
+
/**
|
|
22
|
+
* Strip the `_xlfn.` function prefix and the `_xlpm.` LET-parameter prefix back to the plain names,
|
|
23
|
+
* so the model holds the readable form regardless of how a file stored it. Opaque regions (string
|
|
24
|
+
* literals, sheet names, structured references) are left untouched.
|
|
25
|
+
*/
|
|
26
|
+
export declare function unmangleFunctions(formula: string): string;
|
|
27
|
+
/**
|
|
28
|
+
* Prefix every LET/LAMBDA parameter identifier with `_xlpm.` — at its declaration and at each
|
|
29
|
+
* reference within the binding call's parentheses — so Excel accepts the stored formula. The prefix
|
|
30
|
+
* is lexically scoped: a name is only rewritten inside the call that binds it, opaque regions are
|
|
31
|
+
* copied verbatim, and a lambda-valued parameter used as a call (`f(…)`) is prefixed too. Formulas
|
|
32
|
+
* with no LET/LAMBDA pass through unchanged.
|
|
33
|
+
*/
|
|
34
|
+
export declare function mangleParams(formula: string): string;
|
|
35
|
+
/**
|
|
36
|
+
* Mangle a model formula into its on-disk form: LET/LAMBDA parameter names first (`_xlpm.`), then the
|
|
37
|
+
* modern-function prefix (`_xlfn.`). Ordering matters — parameter mangling reads the plain LET/LAMBDA
|
|
38
|
+
* names before the function pass qualifies them. The inverse for both prefixes is unmangleFunctions.
|
|
39
|
+
*/
|
|
40
|
+
export declare function mangleFormula(formula: string): string;
|
|
41
|
+
/**
|
|
42
|
+
* Shift every relative cell reference in a formula by `colDelta` columns and `rowDelta` rows, leaving
|
|
43
|
+
* absolute (`$`-anchored) axes fixed. This is how a shared-formula clone recovers its own formula from
|
|
44
|
+
* the master's: a master `A1*2` shared one row down reads back as `A2*2`, and `$A$1*B1` shared one row
|
|
45
|
+
* and one column across as `$A$1*C2`. String literals, single-quoted sheet names, and bracketed
|
|
46
|
+
* structured references are copied verbatim, and a sheet-qualified reference shifts the cell while its
|
|
47
|
+
* sheet name is untouched. Function names and defined names carry no row digits, so they pass through.
|
|
48
|
+
*/
|
|
49
|
+
export declare function translateFormula(formula: string, colDelta: number, rowDelta: number): string;
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
// OOXML froze its formula-function grammar around Excel 2007. Every function Microsoft has added
|
|
2
|
+
// since — the dynamic-array family, LAMBDA and its helpers, the newer text and logical functions —
|
|
3
|
+
// is persisted in the sheet XML under an `_xlfn.` name-mangling prefix. The prefix is purely an
|
|
4
|
+
// on-disk convention: the model only ever holds the plain, readable name, the writer applies the
|
|
5
|
+
// prefix on the way out, and the reader strips it back on the way in. A writer that omits it emits
|
|
6
|
+
// a formula current Excel silently drops, because the function is unknown under its bare name. This
|
|
7
|
+
// module is the single place that knows the mangling — shared by the xlsx writer and reader like
|
|
8
|
+
// address.ts and date.ts own their domains.
|
|
9
|
+
//
|
|
10
|
+
// It also owns formula *translation*: a spreadsheet fills a formula down or across a range by storing
|
|
11
|
+
// it once on a master cell and marking the rest as shared clones. Reading a clone means recovering the
|
|
12
|
+
// master's formula shifted to the clone's position — relative references move by the row/column
|
|
13
|
+
// offset, absolute (`$`-anchored) parts stay put. That relative-reference arithmetic lives here too.
|
|
14
|
+
//
|
|
15
|
+
// Every pass over a formula shares one hazard: a comma, paren, function name, or cell reference is
|
|
16
|
+
// mere text when it sits inside a string literal, a single-quoted sheet name, or a bracketed
|
|
17
|
+
// structured reference. `skipOpaque` is the single owner of skipping those regions — every pass drives
|
|
18
|
+
// its string/quote/bracket handling through it, so the rule lives in one place. The stateless passes
|
|
19
|
+
// (function-name and cell-reference rewriting) ride `scanFormula`, which copies the opaque regions
|
|
20
|
+
// verbatim and hands each code run between them to a transform. `mangleParams` is the deliberate
|
|
21
|
+
// exception: LET/LAMBDA parameter scope opens and closes at paren boundaries, state `scanFormula`'s
|
|
22
|
+
// per-run transform cannot carry, so it runs its own forward walk — still deferring to `skipOpaque`.
|
|
23
|
+
import { columnToNumber, numberToColumn } from "./address.js";
|
|
24
|
+
import { MODERN_FUNCTIONS } from "./modern-functions.js";
|
|
25
|
+
const XLFN = '_xlfn.';
|
|
26
|
+
const XLPM = '_xlpm.';
|
|
27
|
+
/**
|
|
28
|
+
* Quote a sheet name for use in a reference exactly when Excel would: a name that is not a plain
|
|
29
|
+
* identifier — or that would read as a cell address — is wrapped in single quotes with its internal
|
|
30
|
+
* quotes doubled, and a simple name is left bare. Shared by everything that *builds* a qualified
|
|
31
|
+
* reference: the `_FilterDatabase` name the writer derives from an autofilter, and the `.xlsb`
|
|
32
|
+
* reader's Ptg decoder, which has only a sheet index to work from and must spell the prefix itself.
|
|
33
|
+
*
|
|
34
|
+
* `last` names the far end of a 3-D span (`Data:More!A1`). A span is quoted as a whole or not at all,
|
|
35
|
+
* because the quotes delimit the sheet *reference* rather than either name — so one awkward endpoint
|
|
36
|
+
* puts both inside the quotes.
|
|
37
|
+
*/
|
|
38
|
+
export function quoteSheetName(name, last) {
|
|
39
|
+
const names = last === undefined ? [name] : [name, last];
|
|
40
|
+
const joined = names.join(':');
|
|
41
|
+
// A sheet name cannot itself contain a colon, so joining first and quoting the result is
|
|
42
|
+
// unambiguous.
|
|
43
|
+
return names.every(isBareSheetName) ? joined : `'${joined.replace(/'/g, "''")}'`;
|
|
44
|
+
}
|
|
45
|
+
function isBareSheetName(name) {
|
|
46
|
+
return /^[A-Za-z_][A-Za-z0-9_.]*$/.test(name) && !/^[A-Za-z]{1,3}\d+$/.test(name);
|
|
47
|
+
}
|
|
48
|
+
// LET and LAMBDA are the only functions that bind names. Their parameter identifiers are persisted
|
|
49
|
+
// under an `_xlpm.` prefix — at the declaration site and at every in-body reference — exactly as the
|
|
50
|
+
// modern functions themselves carry `_xlfn.`. The prefix is scoped: a name bound by one LET/LAMBDA
|
|
51
|
+
// is only prefixed inside that call, so a same-named defined-name reference elsewhere is untouched.
|
|
52
|
+
const SCOPING_FUNCTIONS = new Set(['LET', 'LAMBDA']);
|
|
53
|
+
// A function call is an identifier — dots included, so a dotted name like NORM.DIST is matched whole
|
|
54
|
+
// rather than by its tail — immediately followed by '('. The negative lookbehind rejects a name
|
|
55
|
+
// preceded by an identifier character or '.', so an already-qualified name (`_xlfn.XLOOKUP`) is
|
|
56
|
+
// consumed as a single token whose uppercased form is absent from the set, and is therefore never
|
|
57
|
+
// double-prefixed. Lookbehind rather than a consumed boundary char so adjacent calls
|
|
58
|
+
// (SUM(FILTER(…))) both match.
|
|
59
|
+
const FUNCTION_CALL = /(?<![A-Za-z0-9_.])([A-Za-z_][A-Za-z0-9_.]*)(\s*\()/g;
|
|
60
|
+
const PREFIX = /_xlfn\.|_xlpm\./g;
|
|
61
|
+
// Advance past the opaque region opened at `index`: a double-quoted string literal or a single-quoted
|
|
62
|
+
// sheet name — both honouring the doubled-quote escape (`""`, `''`) — or a bracketed structured
|
|
63
|
+
// reference, which may nest (`Table[[#Data],[Col]]`). Returns the index just past the region, or
|
|
64
|
+
// `index` unchanged when no opaque region opens there. Inside any of the three a comma, paren, function
|
|
65
|
+
// name, or cell reference is inert, so every pass over a formula skips them through this one function.
|
|
66
|
+
function skipOpaque(formula, index) {
|
|
67
|
+
const opener = formula[index];
|
|
68
|
+
const n = formula.length;
|
|
69
|
+
if (opener === '"' || opener === "'") {
|
|
70
|
+
let j = index + 1;
|
|
71
|
+
while (j < n) {
|
|
72
|
+
if (formula[j] === opener) {
|
|
73
|
+
if (formula[j + 1] === opener) {
|
|
74
|
+
j += 2;
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
return j + 1;
|
|
78
|
+
}
|
|
79
|
+
j += 1;
|
|
80
|
+
}
|
|
81
|
+
return n;
|
|
82
|
+
}
|
|
83
|
+
if (opener === '[') {
|
|
84
|
+
let depth = 0;
|
|
85
|
+
let j = index;
|
|
86
|
+
while (j < n) {
|
|
87
|
+
const ch = formula[j];
|
|
88
|
+
if (ch === '[')
|
|
89
|
+
depth += 1;
|
|
90
|
+
else if (ch === ']') {
|
|
91
|
+
depth -= 1;
|
|
92
|
+
if (depth === 0)
|
|
93
|
+
return j + 1;
|
|
94
|
+
}
|
|
95
|
+
j += 1;
|
|
96
|
+
}
|
|
97
|
+
return n;
|
|
98
|
+
}
|
|
99
|
+
return index;
|
|
100
|
+
}
|
|
101
|
+
// Rewrite a formula's code while copying its opaque regions — string literals, single-quoted sheet
|
|
102
|
+
// names, bracketed structured references — verbatim. `transform` sees each maximal run of code between
|
|
103
|
+
// those regions and returns its replacement; the opaque text is never handed to it, so a literal like
|
|
104
|
+
// `"FILTER("` is never mistaken for a call and a `,` inside a structured reference never reads as a
|
|
105
|
+
// separator. Concatenating the transformed runs with the copied regions reproduces the formula.
|
|
106
|
+
function scanFormula(formula, transform) {
|
|
107
|
+
let out = '';
|
|
108
|
+
let codeStart = 0;
|
|
109
|
+
let i = 0;
|
|
110
|
+
const n = formula.length;
|
|
111
|
+
while (i < n) {
|
|
112
|
+
const past = skipOpaque(formula, i);
|
|
113
|
+
if (past > i) {
|
|
114
|
+
out += transform(formula.slice(codeStart, i));
|
|
115
|
+
out += formula.slice(i, past);
|
|
116
|
+
i = past;
|
|
117
|
+
codeStart = past;
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
i += 1;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return out + transform(formula.slice(codeStart));
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Prefix every modern function called by its plain name with `_xlfn.` so Excel accepts the stored
|
|
127
|
+
* formula. Names already prefixed are left alone (never doubled), unknown/legacy functions pass
|
|
128
|
+
* through untouched, and opaque regions (string literals, sheet names, structured references) are
|
|
129
|
+
* preserved verbatim. No other rewriting occurs — in particular no `@` implicit-intersection operator
|
|
130
|
+
* is ever introduced.
|
|
131
|
+
*/
|
|
132
|
+
export function mangleFunctions(formula) {
|
|
133
|
+
return scanFormula(formula, (code) => code.replace(FUNCTION_CALL, (whole, name, open) => MODERN_FUNCTIONS.has(name.toUpperCase()) ? `${XLFN}${name}${open}` : whole));
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Strip the `_xlfn.` function prefix and the `_xlpm.` LET-parameter prefix back to the plain names,
|
|
137
|
+
* so the model holds the readable form regardless of how a file stored it. Opaque regions (string
|
|
138
|
+
* literals, sheet names, structured references) are left untouched.
|
|
139
|
+
*/
|
|
140
|
+
export function unmangleFunctions(formula) {
|
|
141
|
+
return scanFormula(formula, (code) => code.replace(PREFIX, ''));
|
|
142
|
+
}
|
|
143
|
+
const NAME_START = /[A-Za-z_]/;
|
|
144
|
+
const NAME_CHAR = /[A-Za-z0-9_.]/;
|
|
145
|
+
const WHITESPACE = /\s/;
|
|
146
|
+
// Advance past an identifier — dots included, matching FUNCTION_CALL — starting at `i`, or return `i`
|
|
147
|
+
// unchanged when no identifier begins there.
|
|
148
|
+
function readName(formula, i) {
|
|
149
|
+
if (!NAME_START.test(formula[i] ?? ''))
|
|
150
|
+
return i;
|
|
151
|
+
let j = i + 1;
|
|
152
|
+
while (j < formula.length && NAME_CHAR.test(formula[j] ?? ''))
|
|
153
|
+
j += 1;
|
|
154
|
+
return j;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* From the index of a call's opening paren, find the matching close and the `[start, end)` ranges of
|
|
158
|
+
* its top-level, comma-separated arguments. Nested parens are tracked by depth; opaque regions (string
|
|
159
|
+
* literals, sheet names, structured references) are skipped whole so their commas do not split an
|
|
160
|
+
* argument.
|
|
161
|
+
*/
|
|
162
|
+
function parseCall(formula, open) {
|
|
163
|
+
const args = [];
|
|
164
|
+
const n = formula.length;
|
|
165
|
+
let depth = 0;
|
|
166
|
+
let argStart = open + 1;
|
|
167
|
+
let i = open;
|
|
168
|
+
while (i < n) {
|
|
169
|
+
const past = skipOpaque(formula, i);
|
|
170
|
+
if (past > i) {
|
|
171
|
+
i = past;
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
const ch = formula[i];
|
|
175
|
+
if (ch === '(') {
|
|
176
|
+
depth += 1;
|
|
177
|
+
i += 1;
|
|
178
|
+
}
|
|
179
|
+
else if (ch === ')') {
|
|
180
|
+
depth -= 1;
|
|
181
|
+
if (depth === 0) {
|
|
182
|
+
args.push([argStart, i]);
|
|
183
|
+
return { close: i, args };
|
|
184
|
+
}
|
|
185
|
+
i += 1;
|
|
186
|
+
}
|
|
187
|
+
else if (ch === ',' && depth === 1) {
|
|
188
|
+
args.push([argStart, i]);
|
|
189
|
+
argStart = i + 1;
|
|
190
|
+
i += 1;
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
i += 1;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
args.push([argStart, n]);
|
|
197
|
+
return { close: n, args };
|
|
198
|
+
}
|
|
199
|
+
// Extract the single, unprefixed identifier occupying an argument range, or `undefined` when the
|
|
200
|
+
// range is not one clean name (whitespace-trimmed) — a malformed binding we decline to touch.
|
|
201
|
+
function boundName(formula, [start, end]) {
|
|
202
|
+
let s = start;
|
|
203
|
+
let e = end;
|
|
204
|
+
while (s < e && WHITESPACE.test(formula[s] ?? ''))
|
|
205
|
+
s += 1;
|
|
206
|
+
while (e > s && WHITESPACE.test(formula[e - 1] ?? ''))
|
|
207
|
+
e -= 1;
|
|
208
|
+
if (s >= e || readName(formula, s) !== e)
|
|
209
|
+
return undefined;
|
|
210
|
+
const name = formula.slice(s, e);
|
|
211
|
+
return name.startsWith(XLPM) ? undefined : name;
|
|
212
|
+
}
|
|
213
|
+
// The parameter names a LET/LAMBDA call binds. LAMBDA binds every argument but its last (the body);
|
|
214
|
+
// LET binds the even-indexed arguments up to but excluding its last (the calculation).
|
|
215
|
+
function parameterNames(formula, keyword, args) {
|
|
216
|
+
const names = new Set();
|
|
217
|
+
const isLambda = keyword === 'LAMBDA';
|
|
218
|
+
for (let a = 0; a < args.length - 1; a += 1) {
|
|
219
|
+
if (isLambda || a % 2 === 0) {
|
|
220
|
+
const name = boundName(formula, args[a]);
|
|
221
|
+
if (name !== undefined)
|
|
222
|
+
names.add(name);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return names;
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Prefix every LET/LAMBDA parameter identifier with `_xlpm.` — at its declaration and at each
|
|
229
|
+
* reference within the binding call's parentheses — so Excel accepts the stored formula. The prefix
|
|
230
|
+
* is lexically scoped: a name is only rewritten inside the call that binds it, opaque regions are
|
|
231
|
+
* copied verbatim, and a lambda-valued parameter used as a call (`f(…)`) is prefixed too. Formulas
|
|
232
|
+
* with no LET/LAMBDA pass through unchanged.
|
|
233
|
+
*/
|
|
234
|
+
export function mangleParams(formula) {
|
|
235
|
+
// This is the one pass scanFormula cannot serve: it must know when a paren opens a LET/LAMBDA scope
|
|
236
|
+
// and when the matching paren closes it, so it carries frame state across the code between opaque
|
|
237
|
+
// regions rather than transforming each run in isolation. It still defers to skipOpaque, keeping the
|
|
238
|
+
// opaque-skipping rule in one place even though the forward walk here is bespoke.
|
|
239
|
+
let out = '';
|
|
240
|
+
let i = 0;
|
|
241
|
+
const n = formula.length;
|
|
242
|
+
// A stack of active bindings, each expiring exactly at its owner call's close paren. Nested
|
|
243
|
+
// LET/LAMBDA push inner frames that pop first, so shadowing resolves to the same prefix anyway.
|
|
244
|
+
const frames = [];
|
|
245
|
+
const inScope = (name) => frames.some((frame) => frame.names.has(name));
|
|
246
|
+
while (i < n) {
|
|
247
|
+
const top = frames[frames.length - 1];
|
|
248
|
+
if (top !== undefined && i >= top.end) {
|
|
249
|
+
frames.pop();
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
const past = skipOpaque(formula, i);
|
|
253
|
+
if (past > i) {
|
|
254
|
+
out += formula.slice(i, past);
|
|
255
|
+
i = past;
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
const ch = formula[i];
|
|
259
|
+
const nameEnd = readName(formula, i);
|
|
260
|
+
if (nameEnd === i) {
|
|
261
|
+
out += ch;
|
|
262
|
+
i += 1;
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
const name = formula.slice(i, nameEnd);
|
|
266
|
+
let k = nameEnd;
|
|
267
|
+
while (k < n && WHITESPACE.test(formula[k] ?? ''))
|
|
268
|
+
k += 1;
|
|
269
|
+
const heads = formula[k] === '(';
|
|
270
|
+
if (heads && SCOPING_FUNCTIONS.has(name.toUpperCase()) && !inScope(name)) {
|
|
271
|
+
const { close, args } = parseCall(formula, k);
|
|
272
|
+
// The keyword stays at the outer scope; its parameters take effect inside the parens.
|
|
273
|
+
out += formula.slice(i, k + 1);
|
|
274
|
+
frames.push({ end: close, names: parameterNames(formula, name.toUpperCase(), args) });
|
|
275
|
+
i = k + 1;
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
// Any other identifier — a bare reference, an ordinary call, or a lambda-valued parameter call.
|
|
279
|
+
// In-scope names (declaration sites included, as they lie inside their own binding's parens) take
|
|
280
|
+
// the prefix; the rest pass through. Call arguments are covered by the continuing scan, so a
|
|
281
|
+
// nested LET/LAMBDA within them is still seen.
|
|
282
|
+
out += inScope(name) ? `${XLPM}${name}` : name;
|
|
283
|
+
i = nameEnd;
|
|
284
|
+
}
|
|
285
|
+
return out;
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Mangle a model formula into its on-disk form: LET/LAMBDA parameter names first (`_xlpm.`), then the
|
|
289
|
+
* modern-function prefix (`_xlfn.`). Ordering matters — parameter mangling reads the plain LET/LAMBDA
|
|
290
|
+
* names before the function pass qualifies them. The inverse for both prefixes is unmangleFunctions.
|
|
291
|
+
*/
|
|
292
|
+
export function mangleFormula(formula) {
|
|
293
|
+
return mangleFunctions(mangleParams(formula));
|
|
294
|
+
}
|
|
295
|
+
// A relative cell reference to shift: an optional `$`, then 1–3 uppercase column letters, an optional
|
|
296
|
+
// `$`, then the row digits (capped at seven — Excel's last row is 1048576). The column is uppercase-
|
|
297
|
+
// only because Excel stores it that way and so a lowercase defined name is never mistaken for a
|
|
298
|
+
// reference. The lookbehind rejects a reference glued to a preceding name character or '.', so the
|
|
299
|
+
// `A1` inside `_xlfn.A1` or a defined name `FOO_A1` is left alone; the lookahead rejects one continued
|
|
300
|
+
// by a name character, opening a call `(`, or preceding a sheet `!` — a token before `!` is the sheet
|
|
301
|
+
// name (`Q1!A1`), not a cell. Applied per code run, where opaque regions have already been stripped.
|
|
302
|
+
const CELL_REFERENCE = /(?<![A-Za-z0-9_.])(\$?)([A-Z]{1,3})(\$?)([0-9]{1,7})(?![A-Za-z0-9_.!(])/g;
|
|
303
|
+
/**
|
|
304
|
+
* Shift every relative cell reference in a formula by `colDelta` columns and `rowDelta` rows, leaving
|
|
305
|
+
* absolute (`$`-anchored) axes fixed. This is how a shared-formula clone recovers its own formula from
|
|
306
|
+
* the master's: a master `A1*2` shared one row down reads back as `A2*2`, and `$A$1*B1` shared one row
|
|
307
|
+
* and one column across as `$A$1*C2`. String literals, single-quoted sheet names, and bracketed
|
|
308
|
+
* structured references are copied verbatim, and a sheet-qualified reference shifts the cell while its
|
|
309
|
+
* sheet name is untouched. Function names and defined names carry no row digits, so they pass through.
|
|
310
|
+
*/
|
|
311
|
+
export function translateFormula(formula, colDelta, rowDelta) {
|
|
312
|
+
if (colDelta === 0 && rowDelta === 0)
|
|
313
|
+
return formula;
|
|
314
|
+
return scanFormula(formula, (code) => code.replace(CELL_REFERENCE, (_match, colAbs, colLetters, rowAbs, rowDigits) => {
|
|
315
|
+
const col = colAbs === '$' ? colLetters : numberToColumn(columnToNumber(colLetters) + colDelta);
|
|
316
|
+
const row = rowAbs === '$' ? rowDigits : String(Number(rowDigits) + rowDelta);
|
|
317
|
+
return `${colAbs}${col}${rowAbs}${row}`;
|
|
318
|
+
}));
|
|
319
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Cell } from './cell.ts';
|
|
2
|
+
import { type AnchoredImage } from './image.ts';
|
|
3
|
+
import type { MergeRect } from './merge.ts';
|
|
4
|
+
import type { Table } from './table.ts';
|
|
5
|
+
import { type CellValue } from './value.ts';
|
|
6
|
+
import type { ColumnProperties, RowProperties } from './worksheet.ts';
|
|
7
|
+
interface GridStorage {
|
|
8
|
+
readonly rows: Map<number, Map<number, Cell>>;
|
|
9
|
+
readonly rowProperties: Map<number, RowProperties>;
|
|
10
|
+
readonly columns: Map<number, ColumnProperties>;
|
|
11
|
+
readonly merges: string[];
|
|
12
|
+
readonly mergeRects: MergeRect[];
|
|
13
|
+
readonly tables: Table[];
|
|
14
|
+
readonly images: AnchoredImage[];
|
|
15
|
+
}
|
|
16
|
+
export declare class GridEdits {
|
|
17
|
+
#private;
|
|
18
|
+
constructor(storage: GridStorage);
|
|
19
|
+
spliceRows(start: number, count: number, inserted: Map<number, Cell>[]): void;
|
|
20
|
+
spliceColumns(start: number, count: number, inserts: CellValue[][]): void;
|
|
21
|
+
}
|
|
22
|
+
export {};
|