@shbernal/ts-xlsx 1.0.3 → 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.
Files changed (136) hide show
  1. package/README.md +21 -0
  2. package/dist/core/address.js +0 -33
  3. package/dist/core/autofilter.js +0 -9
  4. package/dist/core/cell.d.ts +10 -0
  5. package/dist/core/cell.js +4 -123
  6. package/dist/core/color-resolution.js +0 -60
  7. package/dist/core/column.d.ts +9 -1
  8. package/dist/core/column.js +0 -52
  9. package/dist/core/comment-thread.js +0 -45
  10. package/dist/core/conditional-formatting-overlay.js +0 -11
  11. package/dist/core/conditional-formatting.js +0 -10
  12. package/dist/core/containers.js +0 -15
  13. package/dist/core/data-validation-overlay.js +0 -25
  14. package/dist/core/data-validation.js +0 -7
  15. package/dist/core/date.js +0 -39
  16. package/dist/core/formula.js +0 -117
  17. package/dist/core/grid-edits.js +0 -38
  18. package/dist/core/image.js +0 -25
  19. package/dist/core/internal.js +0 -27
  20. package/dist/core/limits.d.ts +30 -0
  21. package/dist/core/limits.js +2 -0
  22. package/dist/core/merge.d.ts +18 -0
  23. package/dist/core/merge.js +22 -7
  24. package/dist/core/modern-functions.js +0 -17
  25. package/dist/core/page-setup.js +0 -5
  26. package/dist/core/pivot-table.d.ts +1 -1
  27. package/dist/core/pivot-table.js +0 -44
  28. package/dist/core/preserved.js +0 -4
  29. package/dist/core/protection.js +0 -26
  30. package/dist/core/range.js +0 -112
  31. package/dist/core/row-input.d.ts +24 -0
  32. package/dist/core/row-input.js +29 -0
  33. package/dist/core/row.d.ts +9 -1
  34. package/dist/core/row.js +0 -58
  35. package/dist/core/style.js +0 -26
  36. package/dist/core/table-style.js +0 -28
  37. package/dist/core/table.js +0 -97
  38. package/dist/core/text-metrics.d.ts +20 -0
  39. package/dist/core/text-metrics.js +10 -0
  40. package/dist/core/theme.js +0 -85
  41. package/dist/core/value.d.ts +40 -0
  42. package/dist/core/value.js +30 -48
  43. package/dist/core/workbook-protection.js +0 -13
  44. package/dist/core/workbook.d.ts +13 -0
  45. package/dist/core/workbook.js +11 -368
  46. package/dist/core/worksheet-model.js +2 -23
  47. package/dist/core/worksheet.d.ts +22 -0
  48. package/dist/core/worksheet.js +14 -485
  49. package/dist/customui/errors.js +0 -10
  50. package/dist/customui/index.js +0 -5
  51. package/dist/customui/ribbon.js +0 -43
  52. package/dist/entries/core.d.ts +3 -1
  53. package/dist/entries/core.js +3 -7
  54. package/dist/entries/csv.js +0 -4
  55. package/dist/entries/customui.js +0 -2
  56. package/dist/entries/errors.js +0 -13
  57. package/dist/entries/vba.js +0 -5
  58. package/dist/entries/xlsb.js +0 -6
  59. package/dist/entries/xlsx.js +0 -13
  60. package/dist/errors.d.ts +14 -0
  61. package/dist/errors.js +5 -56
  62. package/dist/index.js +0 -12
  63. package/dist/io/csv/read.js +0 -18
  64. package/dist/io/csv/write.js +5 -35
  65. package/dist/io/opc/errors.js +0 -30
  66. package/dist/io/opc/inflate.js +0 -35
  67. package/dist/io/opc/namespaces.js +0 -15
  68. package/dist/io/opc/part-paths.js +0 -11
  69. package/dist/io/opc/read-opc.js +0 -31
  70. package/dist/io/opc/read-options.js +0 -6
  71. package/dist/io/opc/rels.js +0 -13
  72. package/dist/io/opc/sniff-format.js +0 -50
  73. package/dist/io/style/xf-style.js +0 -31
  74. package/dist/io/xlsb/errors.js +0 -10
  75. package/dist/io/xlsb/formula.js +1 -99
  76. package/dist/io/xlsb/primitives.js +1 -83
  77. package/dist/io/xlsb/ptg-functions.js +0 -34
  78. package/dist/io/xlsb/read-shared-strings.js +0 -7
  79. package/dist/io/xlsb/read-styles.js +3 -66
  80. package/dist/io/xlsb/read-worksheet.js +4 -87
  81. package/dist/io/xlsb/read.js +2 -60
  82. package/dist/io/xlsb/record-stream.js +0 -28
  83. package/dist/io/xlsb/record-types.js +0 -19
  84. package/dist/io/xlsx/cell-accumulator.js +0 -32
  85. package/dist/io/xlsx/cell-value.js +0 -29
  86. package/dist/io/xlsx/color-xml.d.ts +6 -0
  87. package/dist/io/xlsx/color-xml.js +42 -0
  88. package/dist/io/xlsx/comments.js +0 -84
  89. package/dist/io/xlsx/conditional-formatting.d.ts +1 -1
  90. package/dist/io/xlsx/conditional-formatting.js +1 -97
  91. package/dist/io/xlsx/data-validation.js +0 -56
  92. package/dist/io/xlsx/edit-vba.js +0 -54
  93. package/dist/io/xlsx/errors.js +0 -12
  94. package/dist/io/xlsx/hyperlinks.js +0 -40
  95. package/dist/io/xlsx/images.js +0 -32
  96. package/dist/io/xlsx/namespaces.js +0 -42
  97. package/dist/io/xlsx/package-plan.js +0 -47
  98. package/dist/io/xlsx/pivot.js +0 -22
  99. package/dist/io/xlsx/{pivot-read.js → read-pivot.js} +0 -30
  100. package/dist/io/xlsx/read-rows.js +1 -83
  101. package/dist/io/xlsx/{shared-strings-read.js → read-shared-strings.js} +0 -11
  102. package/dist/io/xlsx/read-styles.d.ts +25 -1
  103. package/dist/io/xlsx/read-styles.js +61 -108
  104. package/dist/io/xlsx/read-worksheet.js +1 -88
  105. package/dist/io/xlsx/read.js +3 -211
  106. package/dist/io/xlsx/relationships.js +0 -11
  107. package/dist/io/xlsx/rich-runs.js +0 -16
  108. package/dist/io/xlsx/rich-text.js +0 -12
  109. package/dist/io/xlsx/shared-formulas.js +0 -7
  110. package/dist/io/xlsx/shared-strings.js +0 -21
  111. package/dist/io/xlsx/sheet-properties.js +1 -64
  112. package/dist/io/xlsx/styles.d.ts +1 -29
  113. package/dist/io/xlsx/styles.js +2 -366
  114. package/dist/io/xlsx/tables.js +5 -52
  115. package/dist/io/xlsx/threaded-comments.js +0 -103
  116. package/dist/io/xlsx/workbook-xml.js +0 -94
  117. package/dist/io/xlsx/worksheet-xml.js +0 -174
  118. package/dist/io/xlsx/write-stream.js +0 -137
  119. package/dist/io/xlsx/write.js +1 -134
  120. package/dist/io/xlsx/x14-ext.js +0 -8
  121. package/dist/vba/cfb-writer.js +12 -59
  122. package/dist/vba/cfb.js +2 -32
  123. package/dist/vba/codepage.js +1 -23
  124. package/dist/vba/errors.js +0 -15
  125. package/dist/vba/index.js +0 -10
  126. package/dist/vba/ms-ovba.js +3 -46
  127. package/dist/vba/project-editor.js +4 -89
  128. package/dist/vba/project.js +2 -34
  129. package/dist/vba/vba-encoding.js +1 -17
  130. package/dist/xml/errors.js +0 -10
  131. package/dist/xml/xml-read.js +3 -82
  132. package/dist/xml/xml.js +0 -39
  133. package/package.json +9 -10
  134. package/skills/ts-xlsx-upstream/SKILL.md +178 -0
  135. /package/dist/io/xlsx/{pivot-read.d.ts → read-pivot.d.ts} +0 -0
  136. /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
  }
@@ -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, '')
@@ -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;
@@ -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;
@@ -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];
@@ -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');