@shbernal/ts-xlsx 1.1.0 → 1.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 +21 -0
- package/dist/core/address.js +0 -33
- package/dist/core/autofilter.js +0 -9
- package/dist/core/cell.js +0 -131
- package/dist/core/color-resolution.js +0 -60
- package/dist/core/column.js +0 -60
- package/dist/core/comment-thread.js +0 -45
- package/dist/core/conditional-formatting-overlay.js +0 -11
- package/dist/core/conditional-formatting.js +0 -10
- package/dist/core/containers.js +0 -15
- package/dist/core/data-validation-overlay.js +0 -25
- package/dist/core/data-validation.js +0 -7
- package/dist/core/date.js +0 -39
- package/dist/core/formula.js +0 -117
- package/dist/core/grid-edits.js +0 -38
- package/dist/core/image.js +0 -25
- package/dist/core/internal.js +0 -27
- package/dist/core/limits.js +0 -45
- package/dist/core/merge.d.ts +18 -0
- package/dist/core/merge.js +22 -7
- package/dist/core/modern-functions.js +0 -17
- package/dist/core/page-setup.js +0 -5
- package/dist/core/pivot-table.d.ts +1 -1
- package/dist/core/pivot-table.js +0 -48
- package/dist/core/preserved.js +0 -4
- package/dist/core/protection.js +0 -26
- package/dist/core/range.js +0 -112
- package/dist/core/row-input.d.ts +24 -0
- package/dist/core/row-input.js +29 -0
- package/dist/core/row.js +0 -66
- package/dist/core/style.js +0 -26
- package/dist/core/table-style.js +0 -28
- package/dist/core/table.js +0 -97
- package/dist/core/text-metrics.js +0 -40
- package/dist/core/theme.js +0 -85
- package/dist/core/value.js +0 -94
- package/dist/core/workbook-protection.js +0 -13
- package/dist/core/workbook.js +0 -380
- package/dist/core/worksheet-model.js +2 -23
- package/dist/core/worksheet.js +7 -506
- package/dist/customui/errors.js +0 -10
- package/dist/customui/index.js +0 -5
- package/dist/customui/ribbon.js +0 -43
- package/dist/entries/core.js +0 -6
- package/dist/entries/csv.js +0 -4
- package/dist/entries/customui.js +0 -2
- package/dist/entries/errors.js +0 -13
- package/dist/entries/vba.js +0 -5
- package/dist/entries/xlsb.js +0 -6
- package/dist/entries/xlsx.js +0 -13
- package/dist/errors.d.ts +14 -0
- package/dist/errors.js +5 -56
- package/dist/index.js +0 -12
- package/dist/io/csv/read.js +0 -18
- package/dist/io/csv/write.js +0 -18
- package/dist/io/opc/errors.js +0 -30
- package/dist/io/opc/inflate.js +0 -35
- package/dist/io/opc/namespaces.js +0 -15
- package/dist/io/opc/part-paths.js +0 -11
- package/dist/io/opc/read-opc.js +0 -31
- package/dist/io/opc/read-options.js +0 -6
- package/dist/io/opc/rels.js +0 -13
- package/dist/io/opc/sniff-format.js +0 -50
- package/dist/io/style/xf-style.js +0 -31
- package/dist/io/xlsb/errors.js +0 -10
- package/dist/io/xlsb/formula.js +1 -99
- package/dist/io/xlsb/primitives.js +1 -83
- package/dist/io/xlsb/ptg-functions.js +0 -34
- package/dist/io/xlsb/read-shared-strings.js +0 -7
- package/dist/io/xlsb/read-styles.js +3 -66
- package/dist/io/xlsb/read-worksheet.js +4 -87
- package/dist/io/xlsb/read.js +2 -60
- package/dist/io/xlsb/record-stream.js +0 -28
- package/dist/io/xlsb/record-types.js +0 -19
- package/dist/io/xlsx/cell-accumulator.js +0 -32
- package/dist/io/xlsx/cell-value.js +0 -29
- package/dist/io/xlsx/color-xml.d.ts +6 -0
- package/dist/io/xlsx/color-xml.js +42 -0
- package/dist/io/xlsx/comments.js +0 -84
- package/dist/io/xlsx/conditional-formatting.d.ts +1 -1
- package/dist/io/xlsx/conditional-formatting.js +1 -97
- package/dist/io/xlsx/data-validation.js +0 -56
- package/dist/io/xlsx/edit-vba.js +0 -54
- package/dist/io/xlsx/errors.js +0 -12
- package/dist/io/xlsx/hyperlinks.js +0 -40
- package/dist/io/xlsx/images.js +0 -32
- package/dist/io/xlsx/namespaces.js +0 -42
- package/dist/io/xlsx/package-plan.js +0 -47
- package/dist/io/xlsx/pivot.js +0 -22
- package/dist/io/xlsx/{pivot-read.js → read-pivot.js} +0 -30
- package/dist/io/xlsx/read-rows.js +1 -83
- package/dist/io/xlsx/{shared-strings-read.js → read-shared-strings.js} +0 -11
- package/dist/io/xlsx/read-styles.d.ts +25 -1
- package/dist/io/xlsx/read-styles.js +61 -108
- package/dist/io/xlsx/read-worksheet.js +1 -88
- package/dist/io/xlsx/read.js +3 -211
- package/dist/io/xlsx/relationships.js +0 -11
- package/dist/io/xlsx/rich-runs.js +0 -16
- package/dist/io/xlsx/rich-text.js +0 -12
- package/dist/io/xlsx/shared-formulas.js +0 -7
- package/dist/io/xlsx/shared-strings.js +0 -21
- package/dist/io/xlsx/sheet-properties.js +1 -64
- package/dist/io/xlsx/styles.d.ts +1 -29
- package/dist/io/xlsx/styles.js +2 -366
- package/dist/io/xlsx/tables.js +5 -52
- package/dist/io/xlsx/threaded-comments.js +0 -103
- package/dist/io/xlsx/workbook-xml.js +0 -94
- package/dist/io/xlsx/worksheet-xml.js +0 -174
- package/dist/io/xlsx/write-stream.js +0 -137
- package/dist/io/xlsx/write.js +1 -134
- package/dist/io/xlsx/x14-ext.js +0 -8
- package/dist/vba/cfb-writer.js +12 -59
- package/dist/vba/cfb.js +2 -32
- package/dist/vba/codepage.js +1 -23
- package/dist/vba/errors.js +0 -15
- package/dist/vba/index.js +0 -10
- package/dist/vba/ms-ovba.js +3 -46
- package/dist/vba/project-editor.js +4 -89
- package/dist/vba/project.js +2 -34
- package/dist/vba/vba-encoding.js +1 -17
- package/dist/xml/errors.js +0 -10
- package/dist/xml/xml-read.js +3 -82
- package/dist/xml/xml.js +0 -39
- package/package.json +8 -9
- package/skills/ts-xlsx-upstream/SKILL.md +178 -0
- /package/dist/io/xlsx/{pivot-read.d.ts → read-pivot.d.ts} +0 -0
- /package/dist/io/xlsx/{shared-strings-read.d.ts → read-shared-strings.d.ts} +0 -0
|
@@ -1,30 +1,4 @@
|
|
|
1
|
-
// Modern threaded comments in the model — the review-style conversations Excel has written since 2018:
|
|
2
|
-
// an anchored discussion of authored messages with timestamps, replies, a resolved state, and
|
|
3
|
-
// @mentions. Distinct from a legacy note (`cell.note`), which is a single anonymous annotation; Excel's
|
|
4
|
-
// own UI draws the same line, calling these "comments" and those "notes". A cell carries one or the
|
|
5
|
-
// other, never both.
|
|
6
|
-
//
|
|
7
|
-
// The wire form lives in two parts (see `io/xlsx/threaded-comments.ts`): the messages per sheet, and a
|
|
8
|
-
// workbook-level identity registry the messages point into. The model mirrors that split — a thread's
|
|
9
|
-
// authors and mentioned people are {@link Person} entries resolved through
|
|
10
|
-
// {@link Workbook.getPerson}, not names duplicated onto every message.
|
|
11
|
-
// A GUID in any of the spellings a caller plausibly has one in — braced or bare, upper or lower case.
|
|
12
|
-
// `crypto.randomUUID()` produces the bare lower-case form, so accepting only the canonical spelling would
|
|
13
|
-
// reject the one obvious way to make an id in JavaScript.
|
|
14
1
|
const GUID = /^\{?([0-9a-f]{8})-([0-9a-f]{4})-([0-9a-f]{4})-([0-9a-f]{4})-([0-9a-f]{12})\}?$/i;
|
|
15
|
-
/**
|
|
16
|
-
* The one spelling every threaded-comment identifier must take on the wire: brace-wrapped, upper-case hex.
|
|
17
|
-
*
|
|
18
|
-
* Verified against the OOXML schema, which pins `person/@id`, a message's `id`/`personId`/`parentId`, and a
|
|
19
|
-
* mention's `mentionpersonId`/`mentionId` to exactly `\{[0-9A-F]{8}-…\}` — a bare GUID and a lower-case one
|
|
20
|
-
* are each rejected outright. So this normalises rather than merely checking: a caller passing
|
|
21
|
-
* `crypto.randomUUID()` gets a valid file instead of one Excel offers to repair.
|
|
22
|
-
*
|
|
23
|
-
* The authoring path alone goes through here. A file's own ids are re-emitted as the file wrote them, since
|
|
24
|
-
* a reader that rewrote them would break every reference pointing at them.
|
|
25
|
-
*
|
|
26
|
-
* @throws {SyntaxError} if the value is not a GUID in any spelling.
|
|
27
|
-
*/
|
|
28
2
|
export function commentThreadGuid(value, what) {
|
|
29
3
|
const match = GUID.exec(value.trim());
|
|
30
4
|
if (match === null) {
|
|
@@ -33,26 +7,7 @@ export function commentThreadGuid(value, what) {
|
|
|
33
7
|
}
|
|
34
8
|
return `{${match.slice(1).join('-').toUpperCase()}}`;
|
|
35
9
|
}
|
|
36
|
-
/**
|
|
37
|
-
* The largest value a mention offset can take on the wire. Verified against the OOXML schema: both
|
|
38
|
-
* `startIndex` and `length` are `xsd:unsignedInt`, so `4294967295` validates and `4294967296` is rejected
|
|
39
|
-
* as "not a valid 'UInt32' value".
|
|
40
|
-
*
|
|
41
|
-
* Wildly beyond any real message, and that is the point — the ceiling exists so a value from a hostile
|
|
42
|
-
* part can never reach the serialiser. JavaScript spells a large enough number in exponent form
|
|
43
|
-
* (`String(1e21)` is `"1e+21"`), which is not a numeric literal any schema accepts, and one invalid
|
|
44
|
-
* attribute is enough for Excel to offer to repair the whole conversation away.
|
|
45
|
-
*/
|
|
46
10
|
export const MENTION_OFFSET_MAX = 0xffff_ffff;
|
|
47
|
-
/**
|
|
48
|
-
* A mention offset as the wire accepts it: a whole number within {@link MENTION_OFFSET_MAX}.
|
|
49
|
-
*
|
|
50
|
-
* The authoring path alone throws. A file's own mentions are read leniently — one carrying an unusable
|
|
51
|
-
* offset is dropped, keeping the message text and losing only the chip — because a foreign generator's
|
|
52
|
-
* arithmetic is not something a caller can fix, whereas their own is.
|
|
53
|
-
*
|
|
54
|
-
* @throws {SyntaxError} if the value is negative, fractional, or beyond the wire's ceiling.
|
|
55
|
-
*/
|
|
56
11
|
export function commentThreadOffset(value, what) {
|
|
57
12
|
if (!Number.isInteger(value) || value < 0 || value > MENTION_OFFSET_MAX) {
|
|
58
13
|
throw new SyntaxError(`${what} must be a whole number between 0 and ${MENTION_OFFSET_MAX} — a mention's span is ` +
|
|
@@ -1,23 +1,12 @@
|
|
|
1
|
-
// The conditional-formatting overlay a Worksheet owns: an insertion-ordered, defensively-copied list
|
|
2
|
-
// of range-bound rule sets. Kept as its own class, the sibling to {@link DataValidationOverlay}, so
|
|
3
|
-
// Worksheet delegates the collection's storage and cloning rather than managing the array itself.
|
|
4
1
|
import { cloneConditionalFormatting } from './conditional-formatting.js';
|
|
5
2
|
export class ConditionalFormattingOverlay {
|
|
6
3
|
#entries = [];
|
|
7
|
-
/**
|
|
8
|
-
* Attach a conditional formatting to a target range. `formatting.ref` is an OOXML `sqref` — one
|
|
9
|
-
* range (`"A1:A10"`), a whole column, or several space-separated areas (`"A1:C1 A3:C3"`) sharing one
|
|
10
|
-
* rule set. The block is stored once against the range, defensively copied so the getter never hands
|
|
11
|
-
* back a reference into the caller's object.
|
|
12
|
-
*/
|
|
13
4
|
add(formatting) {
|
|
14
5
|
this.#entries.push(cloneConditionalFormatting(formatting));
|
|
15
6
|
}
|
|
16
|
-
/** The conditional formattings on this sheet, each bound to its target range, in insertion order. */
|
|
17
7
|
get entries() {
|
|
18
8
|
return this.#entries;
|
|
19
9
|
}
|
|
20
|
-
/** Drop every conditional formatting, leaving the overlay empty. */
|
|
21
10
|
clear() {
|
|
22
11
|
this.#entries.length = 0;
|
|
23
12
|
}
|
|
@@ -1,13 +1,3 @@
|
|
|
1
|
-
// Conditional formatting — the rules that restyle a cell based on its value (a data bar, a colour
|
|
2
|
-
// scale, a "highlight cells greater than 10", a formula-driven expression). Like a data validation,
|
|
3
|
-
// it is a worksheet-level overlay keyed by a target range, not a facet owned by one cell: one rule
|
|
4
|
-
// covers a whole range, and several rules can layer on the same cells with an evaluation precedence.
|
|
5
|
-
//
|
|
6
|
-
// The model carries the operands each rule type needs and otherwise leaves them absent. A rule type
|
|
7
|
-
// the library does not interpret in depth still round-trips its `type`, `priority`, `operator`,
|
|
8
|
-
// `formulae`, and differential-style reference — so a read/write cycle never silently drops a rule.
|
|
9
|
-
/** A defensive deep copy, so a stored conditional formatting never aliases the caller's object nor
|
|
10
|
-
* any of its nested arrays (rules, formulae, cfvo, colours) or the differential style. */
|
|
11
1
|
export function cloneConditionalFormatting(cf) {
|
|
12
2
|
return { ref: cf.ref, rules: cf.rules.map(cloneRule) };
|
|
13
3
|
}
|
package/dist/core/containers.js
CHANGED
|
@@ -1,23 +1,8 @@
|
|
|
1
|
-
// Identity-preserving replacement of a container's contents.
|
|
2
|
-
//
|
|
3
|
-
// Several model fields are `readonly` fields holding a mutable object or array: a caller (or a live
|
|
4
|
-
// getter) may hold a durable reference to the container, so importing a model must replace what the
|
|
5
|
-
// container *holds* without swapping the container itself. These two primitives do that — one for an
|
|
6
|
-
// object's keys, one for an array's elements — so the field's identity survives a wholesale reload.
|
|
7
|
-
// Replace an object's contents in place: clear every own key, then copy `source`'s keys over. Used for
|
|
8
|
-
// fields like a worksheet's `properties`/`pageSetup`/`headerFooter`, which are `readonly` fields holding
|
|
9
|
-
// mutable objects, so importing a model must overwrite them rather than reassign — and clear any stale
|
|
10
|
-
// key the incoming model does not carry. `Reflect` deletes each existing key without pretending the
|
|
11
|
-
// object carries a string index signature.
|
|
12
1
|
export function overwrite(target, source) {
|
|
13
2
|
for (const key of Reflect.ownKeys(target))
|
|
14
3
|
Reflect.deleteProperty(target, key);
|
|
15
4
|
Object.assign(target, source);
|
|
16
5
|
}
|
|
17
|
-
// Replace an array's contents in place: clear it, then append `next` element by element. Appends
|
|
18
|
-
// individually rather than spreading `next` into a single `push(...next)` call, whose argument count the
|
|
19
|
-
// JS engine bounds by its call-stack limit — these arrays are filled from parsed files, so a hostile
|
|
20
|
-
// input must not be able to overflow that limit.
|
|
21
6
|
export function replaceContents(array, next) {
|
|
22
7
|
array.length = 0;
|
|
23
8
|
for (const item of next)
|
|
@@ -1,27 +1,9 @@
|
|
|
1
|
-
// The data-validation overlay a Worksheet owns: an insertion-ordered list of range-bound rules, plus
|
|
2
|
-
// the decoded rectangles a point-in-rect lookup ({@link DataValidationOverlay.at}) tests against. Kept
|
|
3
|
-
// as its own class — the sibling to {@link GridEdits} that owns splice arithmetic — rather than inline
|
|
4
|
-
// on Worksheet, since a validation's storage (a rule plus its decoded ranges) is a self-contained unit
|
|
5
|
-
// Worksheet only ever adds to, reads, or clears wholesale.
|
|
6
1
|
import { cloneDataValidation, } from './data-validation.js';
|
|
7
2
|
import { decodeSqrefRects } from './merge.js';
|
|
8
3
|
export class DataValidationOverlay {
|
|
9
4
|
#entries = [];
|
|
10
5
|
#rects = [];
|
|
11
|
-
/**
|
|
12
|
-
* Attach a rule to a target range (`"B2:B20"`, a whole column `"B2:B1048576"`, or a space-separated
|
|
13
|
-
* `sqref` of several ranges). The rule is stored once against the range, not copied per covered
|
|
14
|
-
* cell, so a whole-column dropdown stays a single entry. A cell inside the range reports the rule
|
|
15
|
-
* through {@link at}.
|
|
16
|
-
*
|
|
17
|
-
* Pass `{extended: true}` to mark a rule that belongs in the 2009 extension form
|
|
18
|
-
* (`<x14:dataValidation>`) — the carrier Excel uses for a list source on another sheet and other
|
|
19
|
-
* shapes the standard element cannot express. The reader sets it for a rule found in that form so a
|
|
20
|
-
* round-trip writes it back there instead of silently corrupting the cross-sheet reference.
|
|
21
|
-
*/
|
|
22
6
|
add(sqref, rule, options = {}) {
|
|
23
|
-
// One defensive copy, shared by the serialisable entry and the lookup index, so the getter never
|
|
24
|
-
// hands back a reference into the caller's object.
|
|
25
7
|
const stored = cloneDataValidation(rule);
|
|
26
8
|
const entry = { sqref, rule: stored };
|
|
27
9
|
if (options.extended)
|
|
@@ -29,15 +11,9 @@ export class DataValidationOverlay {
|
|
|
29
11
|
this.#entries.push(entry);
|
|
30
12
|
this.#rects.push({ rects: decodeSqrefRects(sqref), rule: stored });
|
|
31
13
|
}
|
|
32
|
-
/** The data validations on this sheet, each bound to its target range, in insertion order. */
|
|
33
14
|
get entries() {
|
|
34
15
|
return this.#entries;
|
|
35
16
|
}
|
|
36
|
-
/**
|
|
37
|
-
* The validation covering the 1-based `col`/`row`, or `undefined` when none does. The first added
|
|
38
|
-
* rule whose range contains the cell wins, mirroring how a spreadsheet resolves overlapping
|
|
39
|
-
* validations.
|
|
40
|
-
*/
|
|
41
17
|
at(col, row) {
|
|
42
18
|
for (const { rects, rule } of this.#rects) {
|
|
43
19
|
for (const rect of rects) {
|
|
@@ -48,7 +24,6 @@ export class DataValidationOverlay {
|
|
|
48
24
|
}
|
|
49
25
|
return undefined;
|
|
50
26
|
}
|
|
51
|
-
/** Drop every validation, leaving the overlay empty. */
|
|
52
27
|
clear() {
|
|
53
28
|
this.#entries.length = 0;
|
|
54
29
|
this.#rects.length = 0;
|
|
@@ -1,10 +1,3 @@
|
|
|
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
1
|
export function cloneDataValidation(rule) {
|
|
9
2
|
return {
|
|
10
3
|
...rule,
|
package/dist/core/date.js
CHANGED
|
@@ -1,54 +1,15 @@
|
|
|
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
1
|
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
2
|
const EPOCH_1900_UTC = Date.UTC(1899, 11, 30);
|
|
19
3
|
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
4
|
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
5
|
export function dateToSerial(date) {
|
|
31
6
|
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
7
|
return days <= PHANTOM_SERIAL ? days - 1 : days;
|
|
35
8
|
}
|
|
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
9
|
export function serialToDate(serial) {
|
|
42
10
|
const dayOffset = serial < PHANTOM_SERIAL ? serial + 1 : serial;
|
|
43
11
|
return new Date(EPOCH_1900_UTC + dayOffset * MS_PER_DAY);
|
|
44
12
|
}
|
|
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
13
|
export function isDateFormat(code) {
|
|
53
14
|
const stripped = code
|
|
54
15
|
.replace(/\[[^\]]*\]/g, '')
|
package/dist/core/formula.js
CHANGED
|
@@ -1,68 +1,18 @@
|
|
|
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
1
|
import { columnToNumber, numberToColumn } from './address.js';
|
|
24
2
|
import { MODERN_FUNCTIONS } from './modern-functions.js';
|
|
25
3
|
const XLFN = '_xlfn.';
|
|
26
4
|
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
5
|
export function quoteSheetName(name, last) {
|
|
39
6
|
const names = last === undefined ? [name] : [name, last];
|
|
40
7
|
const joined = names.join(':');
|
|
41
|
-
// A sheet name cannot itself contain a colon, so joining first and quoting the result is
|
|
42
|
-
// unambiguous.
|
|
43
8
|
return names.every(isBareSheetName) ? joined : `'${joined.replace(/'/g, "''")}'`;
|
|
44
9
|
}
|
|
45
10
|
function isBareSheetName(name) {
|
|
46
11
|
return /^[A-Za-z_][A-Za-z0-9_.]*$/.test(name) && !/^[A-Za-z]{1,3}\d+$/.test(name);
|
|
47
12
|
}
|
|
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
13
|
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
14
|
const FUNCTION_CALL = /(?<![A-Za-z0-9_.])([A-Za-z_][A-Za-z0-9_.]*)(\s*\()/g;
|
|
60
15
|
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
16
|
function skipOpaque(formula, index) {
|
|
67
17
|
const opener = formula[index];
|
|
68
18
|
const n = formula.length;
|
|
@@ -98,11 +48,6 @@ function skipOpaque(formula, index) {
|
|
|
98
48
|
}
|
|
99
49
|
return index;
|
|
100
50
|
}
|
|
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
51
|
function scanFormula(formula, transform) {
|
|
107
52
|
let out = '';
|
|
108
53
|
let codeStart = 0;
|
|
@@ -122,29 +67,15 @@ function scanFormula(formula, transform) {
|
|
|
122
67
|
}
|
|
123
68
|
return out + transform(formula.slice(codeStart));
|
|
124
69
|
}
|
|
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
70
|
export function mangleFunctions(formula) {
|
|
133
71
|
return scanFormula(formula, (code) => code.replace(FUNCTION_CALL, (whole, name, open) => MODERN_FUNCTIONS.has(name.toUpperCase()) ? `${XLFN}${name}${open}` : whole));
|
|
134
72
|
}
|
|
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
73
|
export function unmangleFunctions(formula) {
|
|
141
74
|
return scanFormula(formula, (code) => code.replace(PREFIX, ''));
|
|
142
75
|
}
|
|
143
76
|
const NAME_START = /[A-Za-z_]/;
|
|
144
77
|
const NAME_CHAR = /[A-Za-z0-9_.]/;
|
|
145
78
|
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
79
|
function readName(formula, i) {
|
|
149
80
|
if (!NAME_START.test(formula[i] ?? ''))
|
|
150
81
|
return i;
|
|
@@ -153,12 +84,6 @@ function readName(formula, i) {
|
|
|
153
84
|
j += 1;
|
|
154
85
|
return j;
|
|
155
86
|
}
|
|
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
87
|
function parseCall(formula, open) {
|
|
163
88
|
const args = [];
|
|
164
89
|
const n = formula.length;
|
|
@@ -196,8 +121,6 @@ function parseCall(formula, open) {
|
|
|
196
121
|
args.push([argStart, n]);
|
|
197
122
|
return { close: n, args };
|
|
198
123
|
}
|
|
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
124
|
function boundName(formula, [start, end]) {
|
|
202
125
|
let s = start;
|
|
203
126
|
let e = end;
|
|
@@ -210,8 +133,6 @@ function boundName(formula, [start, end]) {
|
|
|
210
133
|
const name = formula.slice(s, e);
|
|
211
134
|
return name.startsWith(XLPM) ? undefined : name;
|
|
212
135
|
}
|
|
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
136
|
function parameterNames(formula, keyword, args) {
|
|
216
137
|
const names = new Set();
|
|
217
138
|
const isLambda = keyword === 'LAMBDA';
|
|
@@ -224,23 +145,10 @@ function parameterNames(formula, keyword, args) {
|
|
|
224
145
|
}
|
|
225
146
|
return names;
|
|
226
147
|
}
|
|
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
148
|
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
149
|
let out = '';
|
|
240
150
|
let i = 0;
|
|
241
151
|
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
152
|
const frames = [];
|
|
245
153
|
const inScope = (name) => frames.some((frame) => frame.names.has(name));
|
|
246
154
|
while (i < n) {
|
|
@@ -269,45 +177,20 @@ export function mangleParams(formula) {
|
|
|
269
177
|
const heads = formula[k] === '(';
|
|
270
178
|
if (heads && SCOPING_FUNCTIONS.has(name.toUpperCase()) && !inScope(name)) {
|
|
271
179
|
const { close, args } = parseCall(formula, k);
|
|
272
|
-
// The keyword stays at the outer scope; its parameters take effect inside the parens.
|
|
273
180
|
out += formula.slice(i, k + 1);
|
|
274
181
|
frames.push({ end: close, names: parameterNames(formula, name.toUpperCase(), args) });
|
|
275
182
|
i = k + 1;
|
|
276
183
|
continue;
|
|
277
184
|
}
|
|
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
185
|
out += inScope(name) ? `${XLPM}${name}` : name;
|
|
283
186
|
i = nameEnd;
|
|
284
187
|
}
|
|
285
188
|
return out;
|
|
286
189
|
}
|
|
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
190
|
export function mangleFormula(formula) {
|
|
293
191
|
return mangleFunctions(mangleParams(formula));
|
|
294
192
|
}
|
|
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
193
|
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
194
|
export function translateFormula(formula, colDelta, rowDelta) {
|
|
312
195
|
if (colDelta === 0 && rowDelta === 0)
|
|
313
196
|
return formula;
|
package/dist/core/grid-edits.js
CHANGED
|
@@ -1,16 +1,8 @@
|
|
|
1
|
-
// Structural-edit machinery: the splice arithmetic that inserts or deletes whole rows and columns
|
|
2
|
-
// and keeps everything anchored to the grid moving in step — line metadata, merged ranges, tables,
|
|
3
|
-
// anchored images, and shared-formula clones. It is isolated from Worksheet because it is pure grid
|
|
4
|
-
// mechanics: it holds the sheet's storage containers by reference and mutates them in place, and
|
|
5
|
-
// touches none of the public cell API. Worksheet builds the cells an insert introduces, then hands
|
|
6
|
-
// the pre-built rows (or the raw column values) here for the shift.
|
|
7
1
|
import { decodeAddress, decodeRange, encodeAddress } from './address.js';
|
|
8
2
|
import { Cell, copyCellContent } from './cell.js';
|
|
9
3
|
import { replaceContents } from './containers.js';
|
|
10
4
|
import { isOneCellAnchor } from './image.js';
|
|
11
5
|
import { isSharedFormulaValue } from './value.js';
|
|
12
|
-
// The shift rule shared by every re-anchoring pass: a coordinate before the edit stays put, one at or
|
|
13
|
-
// after the edited span shifts by `delta`, and one inside a deleted span clamps to the cut line (`start`).
|
|
14
6
|
function shiftIndex(v, start, count, delta) {
|
|
15
7
|
return v < start ? v : v >= start + count ? v + delta : start;
|
|
16
8
|
}
|
|
@@ -31,10 +23,6 @@ export class GridEdits {
|
|
|
31
23
|
this.#tables = storage.tables;
|
|
32
24
|
this.#images = storage.images;
|
|
33
25
|
}
|
|
34
|
-
// Apply a delete-then-insert to the row grid: surviving rows below the edit shift by
|
|
35
|
-
// `inserted.length - count`, deleted rows drop out, and the pre-built inserted rows land at `start`.
|
|
36
|
-
// Row metadata and merged ranges shift the same way, so a formatting-only row or a covered merge
|
|
37
|
-
// stays aligned with the data it describes.
|
|
38
26
|
spliceRows(start, count, inserted) {
|
|
39
27
|
const delta = inserted.length - count;
|
|
40
28
|
const shifted = new Map();
|
|
@@ -56,10 +44,6 @@ export class GridEdits {
|
|
|
56
44
|
this.#shiftImages('row', start, count, delta);
|
|
57
45
|
this.#reanchorSharedFormulas('row', start, count, delta);
|
|
58
46
|
}
|
|
59
|
-
// Apply a delete-then-insert to the column grid: cells left of the edit stay, cells at or beyond the
|
|
60
|
-
// deleted span shift by `inserts.length - count` carrying their content, and the inserted column
|
|
61
|
-
// values materialise as fresh cells at `start`. Column metadata, merges, tables, images, and
|
|
62
|
-
// shared-formula clones re-anchor the same way.
|
|
63
47
|
spliceColumns(start, count, inserts) {
|
|
64
48
|
const delta = inserts.length - count;
|
|
65
49
|
for (const [row, cols] of this.#rows) {
|
|
@@ -91,8 +75,6 @@ export class GridEdits {
|
|
|
91
75
|
this.#shiftImages('col', start, count, delta);
|
|
92
76
|
this.#reanchorSharedFormulas('col', start, count, delta);
|
|
93
77
|
}
|
|
94
|
-
// Rebuild a row's cells at a new row index. `Cell` fixes its position at construction, so a moved
|
|
95
|
-
// row is a fresh set of cells at `destRow` carrying the originals' content.
|
|
96
78
|
#relocateRow(cols, destRow) {
|
|
97
79
|
const moved = new Map();
|
|
98
80
|
for (const [col, cell] of cols) {
|
|
@@ -107,11 +89,6 @@ export class GridEdits {
|
|
|
107
89
|
}
|
|
108
90
|
return moved;
|
|
109
91
|
}
|
|
110
|
-
// Re-anchor shared-formula clones through a splice on the given axis. A clone stores its master's
|
|
111
|
-
// absolute address; when the splice shifts the master, that stored address goes stale and the writer
|
|
112
|
-
// would reject the clone as orphaned. Applying the same shift the grid used keeps each clone pointed
|
|
113
|
-
// at its master's new cell. A master whose axis coordinate falls in the deleted span clamps to the
|
|
114
|
-
// cut line like a merge edge — a genuinely orphaned clone the writer then reports legibly.
|
|
115
92
|
#reanchorSharedFormulas(axis, start, count, delta) {
|
|
116
93
|
for (const cols of this.#rows.values()) {
|
|
117
94
|
for (const cell of cols.values()) {
|
|
@@ -131,9 +108,6 @@ export class GridEdits {
|
|
|
131
108
|
}
|
|
132
109
|
}
|
|
133
110
|
}
|
|
134
|
-
// Shift a line-metadata map (row properties keyed by row, or column properties keyed by column)
|
|
135
|
-
// through a splice: entries before the edit stay, entries within the deleted span drop, entries
|
|
136
|
-
// after shift by `delta`. Mutates the map in place.
|
|
137
111
|
#shiftLineProperties(map, start, count, delta) {
|
|
138
112
|
const shifted = new Map();
|
|
139
113
|
for (const [index, value] of map) {
|
|
@@ -146,11 +120,6 @@ export class GridEdits {
|
|
|
146
120
|
for (const [index, value] of shifted)
|
|
147
121
|
map.set(index, value);
|
|
148
122
|
}
|
|
149
|
-
// Re-anchor merged ranges through a row or column splice. A range wholly before the edit is
|
|
150
|
-
// untouched; one wholly after shifts by `nInserts - count`; one whose covered rows/columns are
|
|
151
|
-
// entirely deleted is dropped. A range straddling the cut is a genuinely ambiguous geometry — its
|
|
152
|
-
// edges are clamped to the cut line as a best effort. Unbounded whole-row/column merges carry no
|
|
153
|
-
// rectangle and pass through unchanged.
|
|
154
123
|
#shiftMerges(axis, start, count, delta) {
|
|
155
124
|
const shift = (v) => shiftIndex(v, start, count, delta);
|
|
156
125
|
const merges = [];
|
|
@@ -173,19 +142,12 @@ export class GridEdits {
|
|
|
173
142
|
replaceContents(this.#merges, merges);
|
|
174
143
|
replaceContents(this.#mergeRects, rects);
|
|
175
144
|
}
|
|
176
|
-
// Re-pin the sheet's tables through a splice on the given axis, dropping any table a delete leaves
|
|
177
|
-
// with no row to occupy. `Table` owns the shift arithmetic; the sheet only prunes the casualties.
|
|
178
145
|
#shiftTables(axis, start, count, delta) {
|
|
179
146
|
const survivors = this.#tables.filter((table) => axis === 'row'
|
|
180
147
|
? table.shiftRows(start, count, delta)
|
|
181
148
|
: table.shiftColumns(start, count, delta));
|
|
182
149
|
replaceContents(this.#tables, survivors);
|
|
183
150
|
}
|
|
184
|
-
// Re-pin anchored images through a splice. An anchor point moves like a merge edge: a point before
|
|
185
|
-
// the cut stays, one at or after it shifts by `delta`, and one inside a deleted span clamps to the
|
|
186
|
-
// cut line. Grid points are 0-based, so each is converted to the 1-based coordinate the shared
|
|
187
|
-
// shift arithmetic uses and back. An anchor whose points both move keeps its size; an anchor
|
|
188
|
-
// straddling the cut grows or shrinks, matching how Excel reflows a picture across inserted rows.
|
|
189
151
|
#shiftImages(axis, start, count, delta) {
|
|
190
152
|
const shiftPoint = (point) => {
|
|
191
153
|
const zeroBased = axis === 'row' ? point.row : point.col;
|
package/dist/core/image.js
CHANGED
|
@@ -1,27 +1,11 @@
|
|
|
1
|
-
// Anchored images: the model for a picture pinned to a worksheet's grid.
|
|
2
|
-
//
|
|
3
|
-
// The image *bytes* live once on the workbook (a small media registry, addressed by a numeric id);
|
|
4
|
-
// a worksheet then anchors that image to a rectangle of cells. Storing the bytes centrally means the
|
|
5
|
-
// same picture used on two sheets — a logo in a header band, say — is one media part, not two.
|
|
6
|
-
/** EMUs per pixel at Excel's notional 96 DPI (914400 EMU/inch ÷ 96 px/inch). The conversion is
|
|
7
|
-
* DPI-independent by construction: a pixel extent is a fixed physical size regardless of screen. */
|
|
8
1
|
export const PX_TO_EMU = 9525;
|
|
9
|
-
/** Narrow an anchor to its one-cell (fixed-extent) form; the complement is {@link TwoCellAnchor}. */
|
|
10
2
|
export function isOneCellAnchor(anchor) {
|
|
11
3
|
return 'ext' in anchor;
|
|
12
4
|
}
|
|
13
|
-
// Sub-cell anchor geometry. Excel measures a column in characters of the default font (~7 px each at
|
|
14
|
-
// 96 DPI) and a row in points (1/72 inch); a column or row that sets no size falls back to Excel's
|
|
15
|
-
// own defaults. These constants live here, beside the anchor model they serve, rather than in the
|
|
16
|
-
// Worksheet that merely supplies the per-column/row sizes.
|
|
17
5
|
const CHAR_WIDTH_PX = 7;
|
|
18
6
|
const EMU_PER_POINT = 12700;
|
|
19
7
|
const DEFAULT_COL_WIDTH_CHARS = 8.43;
|
|
20
8
|
const DEFAULT_ROW_HEIGHT_POINTS = 15;
|
|
21
|
-
/** Resolve a possibly-fractional anchor point to the cell it floors to plus a sub-cell EMU offset
|
|
22
|
-
* scaled by that cell's real width/height, so `col: 3.5` lands halfway across column 3 regardless of
|
|
23
|
-
* the column's size. An already-integer point keeps a zero offset (unless one was given). The two
|
|
24
|
-
* lookups supply each column/row's size; a size they leave `undefined` falls back to Excel's default. */
|
|
25
9
|
export function resolveAnchorPoint(point, columnWidth, rowHeight) {
|
|
26
10
|
const col = Math.floor(point.col);
|
|
27
11
|
const row = Math.floor(point.row);
|
|
@@ -31,8 +15,6 @@ export function resolveAnchorPoint(point, columnWidth, rowHeight) {
|
|
|
31
15
|
const rowOff = (point.rowOff ?? 0) + Math.round((point.row - row) * rowHeightEmu);
|
|
32
16
|
return { col, row, colOff, rowOff };
|
|
33
17
|
}
|
|
34
|
-
// Leading magic bytes for the raster formats a spreadsheet embeds, most-specific first. Used to infer
|
|
35
|
-
// an extension when the caller supplies none, so a package never declares an `image/undefined` type.
|
|
36
18
|
const IMAGE_MAGIC = [
|
|
37
19
|
{ ext: 'png', sig: [0x89, 0x50, 0x4e, 0x47] },
|
|
38
20
|
{ ext: 'jpeg', sig: [0xff, 0xd8, 0xff] },
|
|
@@ -46,15 +28,8 @@ function sniffImageExtension(data) {
|
|
|
46
28
|
if (sig.every((b, i) => data[i] === b))
|
|
47
29
|
return ext;
|
|
48
30
|
}
|
|
49
|
-
// An unrecognised blob still needs a valid media name and content type; png keeps the package
|
|
50
|
-
// well-formed rather than emitting a `<Default>` with no or a bogus extension.
|
|
51
31
|
return 'png';
|
|
52
32
|
}
|
|
53
|
-
/** Reduce a caller-supplied extension to the bare, lower-case alphanumeric token OOXML expects for a
|
|
54
|
-
* media part's name and `<Default Extension>`. A leading dot (`".png"`), a URL query string
|
|
55
|
-
* (`"png?alt=media"`), or any other separator a real-world filename/URL drags in is stripped to the
|
|
56
|
-
* leading run of alphanumerics; a missing or all-punctuation hint falls back to sniffing the bytes'
|
|
57
|
-
* magic number, so the package is always well-formed. */
|
|
58
33
|
export function normalizeImageExtension(extension, data) {
|
|
59
34
|
if (typeof extension === 'string') {
|
|
60
35
|
const token = extension.toLowerCase().match(/[a-z0-9]+/)?.[0];
|
package/dist/core/internal.js
CHANGED
|
@@ -1,29 +1,2 @@
|
|
|
1
|
-
// The codec's back channel into the model.
|
|
2
|
-
//
|
|
3
|
-
// A reader has to push state into a `Workbook` that no authoring path can produce: a `<dxfs>` table
|
|
4
|
-
// whose indices existing rules already point at, a theme part carried as opaque bytes, a protection
|
|
5
|
-
// credential in finished agile form with no recoverable password. The streaming writer likewise has
|
|
6
|
-
// to drop a row's cells the moment they are serialised. All of that used to be public methods on the
|
|
7
|
-
// model classes — so they shipped in the `.d.ts`, appeared in the generated API reference, and a
|
|
8
|
-
// caller who tried one put the workbook in a state nothing else could reach or repair. There was no
|
|
9
|
-
// authoring/codec boundary at all: the model class *was* the codec's mutation interface.
|
|
10
|
-
//
|
|
11
|
-
// These symbols are that boundary. The operations still live on the classes and still mutate exactly
|
|
12
|
-
// the state they always did — this moves no data — but reaching them requires a symbol that is not on
|
|
13
|
-
// the public barrel and, because `package.json` exposes only the root entry, is not importable from
|
|
14
|
-
// outside the package at all. Inside the tree, the import is the audit trail: `grep` for this module
|
|
15
|
-
// and you have the complete list of code that can restore preserved state.
|
|
16
|
-
//
|
|
17
|
-
// Prefer an ordinary public method whenever a caller could reasonably want the operation. This is for
|
|
18
|
-
// operations that are *only* meaningful mid-deserialisation.
|
|
19
|
-
/**
|
|
20
|
-
* Keys the codec-only operations on `Workbook` and `Worksheet` (see `WorkbookInternals` /
|
|
21
|
-
* `WorksheetInternals`, declared beside their classes).
|
|
22
|
-
*/
|
|
23
1
|
export const INTERNAL = Symbol('ts-xlsx codec channel');
|
|
24
|
-
/**
|
|
25
|
-
* Keys a `Cell`'s link to a named cell style — its OOXML `xfId`. A single hidden property rather than
|
|
26
|
-
* a channel object, so it stays a prototype accessor: cells are the one model class allocated in the
|
|
27
|
-
* millions, and a per-instance internals object would be a real cost for state most cells never carry.
|
|
28
|
-
*/
|
|
29
2
|
export const NAMED_STYLE_ID = Symbol('ts-xlsx named style link');
|