@cj-tech-master/excelts 9.5.8 → 9.5.9
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/dist/browser/modules/excel/utils/col-cache.js +19 -1
- package/dist/browser/modules/excel/xlsx/xform/book/workbook-xform.js +405 -28
- package/dist/cjs/modules/excel/utils/col-cache.js +19 -1
- package/dist/cjs/modules/excel/xlsx/xform/book/workbook-xform.js +405 -28
- package/dist/esm/modules/excel/utils/col-cache.js +19 -1
- package/dist/esm/modules/excel/xlsx/xform/book/workbook-xform.js +405 -28
- package/dist/iife/excelts.iife.js +278 -29
- package/dist/iife/excelts.iife.js.map +1 -1
- package/dist/iife/excelts.iife.min.js +38 -38
- package/package.json +1 -1
|
@@ -51,7 +51,15 @@ const colCache = {
|
|
|
51
51
|
let l3;
|
|
52
52
|
let n = 1;
|
|
53
53
|
if (level >= 4) {
|
|
54
|
-
|
|
54
|
+
// Defensive invariant: Excel's column space (XFD = 16,384) caps at
|
|
55
|
+
// three letters, so neither `l2n` nor `n2l` should ever ask for a
|
|
56
|
+
// higher level. Both callers validate before reaching here; if
|
|
57
|
+
// this branch fires it indicates a programming error in a future
|
|
58
|
+
// caller, not a user input problem — surface that clearly rather
|
|
59
|
+
// than reusing `ColumnOutOfBoundsError` (which would lie about
|
|
60
|
+
// the offending column, since `level` is a letter-count, not a
|
|
61
|
+
// column number).
|
|
62
|
+
throw new Error(`colCache._fill: invariant violated — level ${level} exceeds the 3-letter cap; callers must validate before invoking _fill`);
|
|
55
63
|
}
|
|
56
64
|
if (this._l2nFill < 1 && level >= 1) {
|
|
57
65
|
while (n <= 26) {
|
|
@@ -92,6 +100,16 @@ const colCache = {
|
|
|
92
100
|
},
|
|
93
101
|
l2n(l) {
|
|
94
102
|
if (!this._l2n[l]) {
|
|
103
|
+
// Excel's column space stops at XFD (16,384) — three letters is
|
|
104
|
+
// the maximum width any valid column letter can have. Reject
|
|
105
|
+
// longer inputs explicitly here, BEFORE handing the length to
|
|
106
|
+
// `_fill`, so the thrown error carries the actual offending
|
|
107
|
+
// letter (`AAAA`) rather than the level integer (`4`) — matching
|
|
108
|
+
// what the equivalent `n2l(n > 16384)` and `decodeAddress` paths
|
|
109
|
+
// already report.
|
|
110
|
+
if (l.length > 3) {
|
|
111
|
+
throw new ColumnOutOfBoundsError(l, "Excel supports columns from 1 to 16384");
|
|
112
|
+
}
|
|
95
113
|
this._fill(l.length);
|
|
96
114
|
}
|
|
97
115
|
if (!this._l2n[l]) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { RowOutOfBoundsError } from "../../../errors.js";
|
|
1
2
|
import { colCache } from "../../../utils/col-cache.js";
|
|
2
3
|
import { resolveRelTarget } from "../../../utils/ooxml-paths.js";
|
|
3
4
|
import { BaseXform } from "../base-xform.js";
|
|
@@ -89,43 +90,70 @@ class WorkbookXform extends BaseXform {
|
|
|
89
90
|
model.sheets = withIndex.map(entry => entry.sheet);
|
|
90
91
|
}
|
|
91
92
|
// collate all the print areas from all of the sheets and add them to the defined names
|
|
93
|
+
//
|
|
94
|
+
// OOXML (ECMA-376 §18.2.5) requires that the (name, localSheetId) pair
|
|
95
|
+
// is unique within `<definedNames>` — so multiple print areas on the
|
|
96
|
+
// same sheet must collapse into a *single* `<definedName>` whose text
|
|
97
|
+
// is a comma-separated list of ranges (the format Excel itself emits).
|
|
98
|
+
// The `printArea` field uses `&&` as the multi-range separator (a
|
|
99
|
+
// historical convention preserved for backwards compatibility); we
|
|
100
|
+
// also accept commas so users can paste Excel's native format.
|
|
101
|
+
//
|
|
102
|
+
// Both `printArea` and the `printTitlesRow`/`printTitlesColumn` fields
|
|
103
|
+
// are normalised through `parsePrintReference` before being emitted.
|
|
104
|
+
// This means the writer accepts any of the forms Excel itself accepts
|
|
105
|
+
// — `A1`, `A1:B5`, `$A$1:$B$5`, `a1:b5`, ` A1 : B5 `, `Sheet!A1:B5`,
|
|
106
|
+
// `'Q,F'!A1:B5`, whole-row `1:5`, whole-column `A:C` — and always
|
|
107
|
+
// emits the canonical `$col$row[:$col$row]` shape that Excel
|
|
108
|
+
// round-trips faithfully. Without normalisation, the previous
|
|
109
|
+
// string-concatenation path produced `$$A$1` for a `$A$1` input
|
|
110
|
+
// (corrupt), `$a1` for a `a1` input (Excel rejects), and
|
|
111
|
+
// `$A1:$B5` row-relative (semantically wrong for a print area).
|
|
92
112
|
const printAreas = [];
|
|
93
113
|
let index = 0; // sheets is sparse array - calc index manually
|
|
94
114
|
model.sheets.forEach((sheet) => {
|
|
95
115
|
if (sheet.pageSetup && sheet.pageSetup.printArea) {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
116
|
+
const ranges = [];
|
|
117
|
+
// Split on either `&&` (legacy excelts separator) or `,` (Excel's
|
|
118
|
+
// native separator) at the *top level* — commas / `&&` inside a
|
|
119
|
+
// quoted sheet name (`'Q1, Forecast'!A1:B5`) must NOT be treated
|
|
120
|
+
// as separators. A naive `split(/&&|,/)` shreds such inputs.
|
|
121
|
+
for (const segment of splitPrintAreaInput(sheet.pageSetup.printArea)) {
|
|
122
|
+
const normalised = normalisePrintAreaRange(segment, sheet.name);
|
|
123
|
+
if (normalised) {
|
|
124
|
+
ranges.push(normalised);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (ranges.length > 0) {
|
|
128
|
+
printAreas.push({
|
|
101
129
|
name: "_xlnm.Print_Area",
|
|
102
|
-
ranges
|
|
130
|
+
ranges,
|
|
103
131
|
localSheetId: index
|
|
104
|
-
};
|
|
105
|
-
|
|
106
|
-
});
|
|
132
|
+
});
|
|
133
|
+
}
|
|
107
134
|
}
|
|
108
135
|
if (sheet.pageSetup &&
|
|
109
136
|
(sheet.pageSetup.printTitlesRow || sheet.pageSetup.printTitlesColumn)) {
|
|
110
137
|
const ranges = [];
|
|
111
138
|
if (sheet.pageSetup.printTitlesColumn) {
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
139
|
+
const normalised = normalisePrintTitlesAxis(sheet.pageSetup.printTitlesColumn, sheet.name);
|
|
140
|
+
if (normalised) {
|
|
141
|
+
ranges.push(normalised);
|
|
142
|
+
}
|
|
116
143
|
}
|
|
117
144
|
if (sheet.pageSetup.printTitlesRow) {
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
145
|
+
const normalised = normalisePrintTitlesAxis(sheet.pageSetup.printTitlesRow, sheet.name);
|
|
146
|
+
if (normalised) {
|
|
147
|
+
ranges.push(normalised);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
if (ranges.length > 0) {
|
|
151
|
+
printAreas.push({
|
|
152
|
+
name: "_xlnm.Print_Titles",
|
|
153
|
+
ranges,
|
|
154
|
+
localSheetId: index
|
|
155
|
+
});
|
|
122
156
|
}
|
|
123
|
-
const definedName = {
|
|
124
|
-
name: "_xlnm.Print_Titles",
|
|
125
|
-
ranges,
|
|
126
|
-
localSheetId: index
|
|
127
|
-
};
|
|
128
|
-
printAreas.push(definedName);
|
|
129
157
|
}
|
|
130
158
|
index++;
|
|
131
159
|
});
|
|
@@ -331,10 +359,14 @@ class WorkbookXform extends BaseXform {
|
|
|
331
359
|
model.definedNames.forEach((definedName) => {
|
|
332
360
|
// For print area/titles, use rawText to extract ranges since the xform
|
|
333
361
|
// layer no longer pre-classifies content (two-phase design).
|
|
362
|
+
// When falling back to rawText we must split on top-level commas
|
|
363
|
+
// (commas inside a quoted sheet name like `'Q1, Forecast'!$A$1` do
|
|
364
|
+
// *not* delimit ranges), so a naive `rawText.split(",")` is wrong
|
|
365
|
+
// and would mis-split sheet names with embedded commas.
|
|
334
366
|
const effectiveRanges = definedName.ranges?.length > 0
|
|
335
367
|
? definedName.ranges
|
|
336
368
|
: definedName.rawText
|
|
337
|
-
?
|
|
369
|
+
? splitPrintAreaInput(definedName.rawText)
|
|
338
370
|
: [];
|
|
339
371
|
if (definedName.name === "_xlnm.Print_Area") {
|
|
340
372
|
worksheet = worksheets[definedName.localSheetId];
|
|
@@ -342,10 +374,52 @@ class WorkbookXform extends BaseXform {
|
|
|
342
374
|
if (!worksheet.pageSetup) {
|
|
343
375
|
worksheet.pageSetup = {};
|
|
344
376
|
}
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
377
|
+
// A print-area `<definedName>` may carry multiple ranges as a
|
|
378
|
+
// comma-separated list (Excel's native format) — read every
|
|
379
|
+
// range, not just the first. Rejoin with `&&` so the
|
|
380
|
+
// worksheet-level `printArea` field uses the legacy excelts
|
|
381
|
+
// separator (preserved for backwards compatibility on the
|
|
382
|
+
// public API; both `&&` and `,` are accepted on write).
|
|
383
|
+
//
|
|
384
|
+
// Route through the same `parsePrintReference` the writer
|
|
385
|
+
// uses so every legitimate Excel reference shape (cell,
|
|
386
|
+
// range, whole-row, whole-column) round-trips. The previous
|
|
387
|
+
// implementation called `colCache.decodeEx` directly, which
|
|
388
|
+
// returns a `NaN`-laced result for whole-row/column inputs
|
|
389
|
+
// (those are not cell addresses) — those legitimate shapes
|
|
390
|
+
// came back as `"NaN:NaN"` on the worksheet model.
|
|
391
|
+
const decoded = [];
|
|
392
|
+
for (const rangeStr of effectiveRanges) {
|
|
393
|
+
// Wrap in try/catch: `parsePrintReference` throws
|
|
394
|
+
// `ColumnOutOfBoundsError` (column past XFD) or
|
|
395
|
+
// `RowOutOfBoundsError` (row 0 or row past 1048576) for
|
|
396
|
+
// out-of-range refs. On the *write* side that throw
|
|
397
|
+
// surfaces a user error, but on this *read* side a
|
|
398
|
+
// malformed file (or one authored by another tool) must
|
|
399
|
+
// not blow up the whole load — drop the bad range and
|
|
400
|
+
// continue.
|
|
401
|
+
let ref;
|
|
402
|
+
try {
|
|
403
|
+
ref = parsePrintReference(rangeStr);
|
|
404
|
+
}
|
|
405
|
+
catch {
|
|
406
|
+
ref = undefined;
|
|
407
|
+
}
|
|
408
|
+
if (!ref) {
|
|
409
|
+
continue;
|
|
410
|
+
}
|
|
411
|
+
// Promote a bare cell to a degenerate range so the
|
|
412
|
+
// worksheet `printArea` field is always a range string —
|
|
413
|
+
// that's the documented API contract and matches what
|
|
414
|
+
// Excel itself emits for single-cell print areas.
|
|
415
|
+
decoded.push(ref.kind === "cell" ? `${ref.dimensions}:${ref.dimensions}` : ref.dimensions);
|
|
416
|
+
}
|
|
417
|
+
if (decoded.length > 0) {
|
|
418
|
+
const joined = decoded.join("&&");
|
|
419
|
+
worksheet.pageSetup.printArea = worksheet.pageSetup.printArea
|
|
420
|
+
? `${worksheet.pageSetup.printArea}&&${joined}`
|
|
421
|
+
: joined;
|
|
422
|
+
}
|
|
349
423
|
}
|
|
350
424
|
}
|
|
351
425
|
else if (definedName.name === "_xlnm.Print_Titles") {
|
|
@@ -402,4 +476,307 @@ WorkbookXform.STATIC_XFORMS = {
|
|
|
402
476
|
$: { appName: "xl", lastEdited: 5, lowestEdited: 5, rupBuild: 9303 }
|
|
403
477
|
})
|
|
404
478
|
};
|
|
479
|
+
/**
|
|
480
|
+
* Split a print-area string on its multi-range separators while honouring
|
|
481
|
+
* single-quoted sheet name segments.
|
|
482
|
+
*
|
|
483
|
+
* Used by both the writer (parsing user-supplied `printArea` values) and
|
|
484
|
+
* the reader (parsing the body of an OOXML `<definedName>` when the
|
|
485
|
+
* defined-name layer hands us the raw text). Recognises both:
|
|
486
|
+
* - `,` — the OOXML / Excel-native separator
|
|
487
|
+
* - `&&` — the legacy excelts convention preserved on the public API
|
|
488
|
+
*
|
|
489
|
+
* Quoted sheet names (`'Q1, Forecast'!A1:B5`) are skipped over: any `,`,
|
|
490
|
+
* `&`, or `'` inside a quoted name is preserved verbatim. A doubled
|
|
491
|
+
* apostrophe inside a quoted segment (`''`) is the OOXML escape for a
|
|
492
|
+
* literal apostrophe and does not terminate the quote.
|
|
493
|
+
*
|
|
494
|
+
* Empty / whitespace-only segments are dropped; the caller normalises
|
|
495
|
+
* each surviving segment further with `parsePrintReference`.
|
|
496
|
+
*/
|
|
497
|
+
function splitPrintAreaInput(input) {
|
|
498
|
+
const result = [];
|
|
499
|
+
let current = "";
|
|
500
|
+
let inQuote = false;
|
|
501
|
+
for (let i = 0; i < input.length; i++) {
|
|
502
|
+
const ch = input[i];
|
|
503
|
+
if (ch === "'") {
|
|
504
|
+
// Doubled apostrophe inside a quoted segment is an escaped literal.
|
|
505
|
+
if (inQuote && input[i + 1] === "'") {
|
|
506
|
+
current += "''";
|
|
507
|
+
i++;
|
|
508
|
+
continue;
|
|
509
|
+
}
|
|
510
|
+
inQuote = !inQuote;
|
|
511
|
+
current += ch;
|
|
512
|
+
continue;
|
|
513
|
+
}
|
|
514
|
+
if (!inQuote) {
|
|
515
|
+
if (ch === ",") {
|
|
516
|
+
if (current.trim()) {
|
|
517
|
+
result.push(current);
|
|
518
|
+
}
|
|
519
|
+
current = "";
|
|
520
|
+
continue;
|
|
521
|
+
}
|
|
522
|
+
if (ch === "&" && input[i + 1] === "&") {
|
|
523
|
+
if (current.trim()) {
|
|
524
|
+
result.push(current);
|
|
525
|
+
}
|
|
526
|
+
current = "";
|
|
527
|
+
i++; // skip the second `&`
|
|
528
|
+
continue;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
current += ch;
|
|
532
|
+
}
|
|
533
|
+
if (current.trim()) {
|
|
534
|
+
result.push(current);
|
|
535
|
+
}
|
|
536
|
+
return result;
|
|
537
|
+
}
|
|
538
|
+
/**
|
|
539
|
+
* Quote a sheet name for inclusion in an OOXML defined-name reference.
|
|
540
|
+
*
|
|
541
|
+
* Per ECMA-376 §18.17 sheet names that contain spaces or any character
|
|
542
|
+
* outside `[A-Za-z0-9_]` MUST be wrapped in single quotes; a literal
|
|
543
|
+
* apostrophe inside the name is doubled. We always quote — over-quoting
|
|
544
|
+
* is harmless (Excel parses both forms) and keeps the writer trivial.
|
|
545
|
+
*/
|
|
546
|
+
function quoteSheetName(sheetName) {
|
|
547
|
+
return `'${sheetName.replace(/'/g, "''")}'`;
|
|
548
|
+
}
|
|
549
|
+
/**
|
|
550
|
+
* Find the index of the first `!` that lies outside any single-quoted
|
|
551
|
+
* sheet-name segment, or `-1` if no unquoted `!` is present.
|
|
552
|
+
*
|
|
553
|
+
* Sheet names quoted with `'` may contain unbalanced characters, so we
|
|
554
|
+
* walk the string honouring quote toggles before declaring a `!` to be
|
|
555
|
+
* the sheet/address separator. Doubled apostrophes (`''`) inside a
|
|
556
|
+
* quoted name are treated as a literal apostrophe per OOXML.
|
|
557
|
+
*/
|
|
558
|
+
function findUnquotedExclamation(value) {
|
|
559
|
+
let inQuote = false;
|
|
560
|
+
for (let i = 0; i < value.length; i++) {
|
|
561
|
+
const ch = value[i];
|
|
562
|
+
if (ch === "'") {
|
|
563
|
+
if (inQuote && value[i + 1] === "'") {
|
|
564
|
+
i++;
|
|
565
|
+
continue;
|
|
566
|
+
}
|
|
567
|
+
inQuote = !inQuote;
|
|
568
|
+
}
|
|
569
|
+
else if (ch === "!" && !inQuote) {
|
|
570
|
+
return i;
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
return -1;
|
|
574
|
+
}
|
|
575
|
+
// Excel 2007+ row limit. Print-area / print-titles references emitted
|
|
576
|
+
// to OOXML must respect this — Excel rejects definitions that point
|
|
577
|
+
// past the addressable sheet, so the writer normalises every row
|
|
578
|
+
// through `parseRowToken` (which throws `RowOutOfBoundsError` on
|
|
579
|
+
// overflow). The rest of the codebase tolerates higher row numbers in
|
|
580
|
+
// transient API calls (`getCell("A99999999")`) because those never
|
|
581
|
+
// reach the file format; print references do.
|
|
582
|
+
const EXCEL_MAX_ROW = 1048576;
|
|
583
|
+
/**
|
|
584
|
+
* Parse a row token (the digits portion of a cell reference, or a
|
|
585
|
+
* whole-row number) into a canonical integer string.
|
|
586
|
+
*
|
|
587
|
+
* Rejects:
|
|
588
|
+
* - row 0 (Excel rows are 1-indexed; `$A$0` is never a valid Excel ref)
|
|
589
|
+
* - rows beyond `EXCEL_MAX_ROW` (Excel hard limit)
|
|
590
|
+
*
|
|
591
|
+
* Normalises:
|
|
592
|
+
* - leading zeros (`001` → `1`) — OOXML expects bare integers, and
|
|
593
|
+
* `Number(...)` collapses any padding the user typed.
|
|
594
|
+
*/
|
|
595
|
+
function parseRowToken(token) {
|
|
596
|
+
// The caller has already matched the token against `\d+`, so `Number`
|
|
597
|
+
// is safe (no NaN). We re-emit the canonical decimal form to drop
|
|
598
|
+
// leading zeros the user typed.
|
|
599
|
+
const n = Number(token);
|
|
600
|
+
if (n < 1) {
|
|
601
|
+
throw new RowOutOfBoundsError(n, `Excel rows are 1-indexed; row ${token} is invalid`);
|
|
602
|
+
}
|
|
603
|
+
if (n > EXCEL_MAX_ROW) {
|
|
604
|
+
throw new RowOutOfBoundsError(n, `Excel supports rows from 1 to ${EXCEL_MAX_ROW}`);
|
|
605
|
+
}
|
|
606
|
+
return String(n);
|
|
607
|
+
}
|
|
608
|
+
/**
|
|
609
|
+
* Parse a single user- or OOXML-supplied print reference into one of
|
|
610
|
+
* Excel's four valid shapes (cell / range / row / column), discarding
|
|
611
|
+
* any sheet prefix the input carries. Both the writer and the reader
|
|
612
|
+
* route through this single parser so the two sides agree on what is
|
|
613
|
+
* accepted and how it is canonicalised.
|
|
614
|
+
*
|
|
615
|
+
* Accepts every form Excel itself accepts on input, regardless of which
|
|
616
|
+
* side calls it:
|
|
617
|
+
* - cell: `A1`, `$A$1`, `Sheet1!$A$1`, `'Q,F'!$A$1`, `a1`
|
|
618
|
+
* - range: `A1:B5`, `$A$1:$B$5`, `Sheet!A1:B5`, ` A1 : B5 `, `a1:b5`,
|
|
619
|
+
* mixed `$A1:$B$5`, reversed `B5:A1` (canonicalised to `A1:B5`)
|
|
620
|
+
* - whole row: `1:5`, `$1:$5`, `Sheet!$1:$5`, `5` (single row),
|
|
621
|
+
* reversed `5:1` (canonicalised to `1:5`), padded `001:005`
|
|
622
|
+
* - whole column: `A:C`, `$A:$C`, `Sheet!$A:$C`, `C` (single column),
|
|
623
|
+
* `a:c`, reversed `C:A` (canonicalised to `A:C`)
|
|
624
|
+
*
|
|
625
|
+
* Returns `undefined` for inputs that do not match one of the four
|
|
626
|
+
* shapes — callers drop the entry rather than emit corrupt XML.
|
|
627
|
+
*
|
|
628
|
+
* **Throws**:
|
|
629
|
+
* - `ColumnOutOfBoundsError` when the input parses as a valid shape
|
|
630
|
+
* but references a column letter beyond Excel's XFD (16384) limit.
|
|
631
|
+
* - `RowOutOfBoundsError` for row 0 (Excel rows are 1-indexed) or
|
|
632
|
+
* rows beyond Excel's `1048576` limit.
|
|
633
|
+
*
|
|
634
|
+
* Both errors match what `getCell` and `colCache.l2n` already throw for
|
|
635
|
+
* the same inputs; surfacing them here means a user who hand-authors a
|
|
636
|
+
* malformed `printArea` finds out at write time rather than producing
|
|
637
|
+
* a workbook Excel silently rejects.
|
|
638
|
+
*
|
|
639
|
+
* Why a hand-rolled parser instead of `colCache.decodeEx`? `decodeEx`
|
|
640
|
+
* was designed for cell addresses and produces `NaN`-laced output for
|
|
641
|
+
* whole-row (`$1:$5`) and whole-column (`$A:$C`) references. Print
|
|
642
|
+
* areas and print titles legitimately use both, so we need a parser
|
|
643
|
+
* that recognises all four shapes uniformly *and* canonicalises
|
|
644
|
+
* reversed endpoints (which `decodeEx` does for cells but not for
|
|
645
|
+
* row/column references).
|
|
646
|
+
*/
|
|
647
|
+
function parsePrintReference(input) {
|
|
648
|
+
if (typeof input !== "string") {
|
|
649
|
+
return undefined;
|
|
650
|
+
}
|
|
651
|
+
const trimmed = input.trim();
|
|
652
|
+
if (!trimmed) {
|
|
653
|
+
return undefined;
|
|
654
|
+
}
|
|
655
|
+
// Strip the sheet prefix if present (we anchor by `localSheetId`,
|
|
656
|
+
// never by the prefix). Any prefix the caller supplies is discarded.
|
|
657
|
+
const exclamation = findUnquotedExclamation(trimmed);
|
|
658
|
+
const body = exclamation === -1 ? trimmed : trimmed.slice(exclamation + 1);
|
|
659
|
+
// Strip every `$`, every whitespace, and upper-case the remaining
|
|
660
|
+
// letters in one pass. This subsumes mixed/redundant `$` signs
|
|
661
|
+
// (`$A1:$B$5` → `A1:B5`), surrounding/internal whitespace
|
|
662
|
+
// (`A1 : B5` → `A1:B5`), and lowercase columns (`a1:b5` → `A1:B5`).
|
|
663
|
+
const cleaned = body.replace(/[\s$]+/g, "").toUpperCase();
|
|
664
|
+
if (!cleaned) {
|
|
665
|
+
return undefined;
|
|
666
|
+
}
|
|
667
|
+
const parts = cleaned.split(":");
|
|
668
|
+
if (parts.length > 2) {
|
|
669
|
+
return undefined;
|
|
670
|
+
}
|
|
671
|
+
const startRaw = parts[0];
|
|
672
|
+
const endRaw = parts.length === 2 ? parts[1] : startRaw;
|
|
673
|
+
// Cell shape: both endpoints are full `<col><row>` addresses.
|
|
674
|
+
const cellRe = /^([A-Z]+)(\d+)$/;
|
|
675
|
+
const startCell = cellRe.exec(startRaw);
|
|
676
|
+
const endCell = cellRe.exec(endRaw);
|
|
677
|
+
if (startCell && endCell) {
|
|
678
|
+
// Validate column letters against Excel's XFD (16384) limit and
|
|
679
|
+
// rows against the `1..1048576` band. `l2n` and `parseRowToken`
|
|
680
|
+
// throw the project-standard errors here so the caller (and end
|
|
681
|
+
// user) sees a familiar diagnosis when they hand-author a bad ref.
|
|
682
|
+
// We keep the column numbers around to canonicalise reversed
|
|
683
|
+
// endpoints below without a second lookup.
|
|
684
|
+
const startColNum = colCache.l2n(startCell[1]);
|
|
685
|
+
const endColNum = colCache.l2n(endCell[1]);
|
|
686
|
+
const startRow = Number(parseRowToken(startCell[2]));
|
|
687
|
+
const endRow = Number(parseRowToken(endCell[2]));
|
|
688
|
+
// Canonicalise reversed endpoints. Excel's UI never produces
|
|
689
|
+
// `B5:A1`, but a hand-authored input might; downstream consumers
|
|
690
|
+
// (PDF layout, the OOXML reader's sort comparators) assume
|
|
691
|
+
// top-left → bottom-right ordering, so we sort here once.
|
|
692
|
+
const tlCol = startColNum <= endColNum ? startCell[1] : endCell[1];
|
|
693
|
+
const brCol = startColNum <= endColNum ? endCell[1] : startCell[1];
|
|
694
|
+
const tlRow = startRow <= endRow ? startRow : endRow;
|
|
695
|
+
const brRow = startRow <= endRow ? endRow : startRow;
|
|
696
|
+
// A bare cell (no `:` in the input) is the only true `cell` shape.
|
|
697
|
+
// `A1:A1` — a `:`-bearing range whose endpoints happen to coincide —
|
|
698
|
+
// is reported as `range`, matching the user's typed intent and
|
|
699
|
+
// avoiding the question of whether `parts.length === 2 && tl === br`
|
|
700
|
+
// should round-trip as a cell or a degenerate range.
|
|
701
|
+
if (parts.length === 1) {
|
|
702
|
+
return {
|
|
703
|
+
kind: "cell",
|
|
704
|
+
ooxml: `$${tlCol}$${tlRow}`,
|
|
705
|
+
dimensions: `${tlCol}${tlRow}`
|
|
706
|
+
};
|
|
707
|
+
}
|
|
708
|
+
return {
|
|
709
|
+
kind: "range",
|
|
710
|
+
ooxml: `$${tlCol}$${tlRow}:$${brCol}$${brRow}`,
|
|
711
|
+
dimensions: `${tlCol}${tlRow}:${brCol}${brRow}`
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
// Whole-row shape: both endpoints are bare row numbers.
|
|
715
|
+
if (/^\d+$/.test(startRaw) && /^\d+$/.test(endRaw)) {
|
|
716
|
+
const startRow = Number(parseRowToken(startRaw));
|
|
717
|
+
const endRow = Number(parseRowToken(endRaw));
|
|
718
|
+
const tl = Math.min(startRow, endRow);
|
|
719
|
+
const br = Math.max(startRow, endRow);
|
|
720
|
+
return {
|
|
721
|
+
kind: "row",
|
|
722
|
+
ooxml: `$${tl}:$${br}`,
|
|
723
|
+
dimensions: `${tl}:${br}`
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
// Whole-column shape: both endpoints are bare column letters. We
|
|
727
|
+
// reuse `l2n`'s already-validated index to canonicalise reversed
|
|
728
|
+
// endpoints — `colCache.l2n` is the project-wide source of truth for
|
|
729
|
+
// column ordering.
|
|
730
|
+
if (/^[A-Z]+$/.test(startRaw) && /^[A-Z]+$/.test(endRaw)) {
|
|
731
|
+
const startNum = colCache.l2n(startRaw);
|
|
732
|
+
const endNum = colCache.l2n(endRaw);
|
|
733
|
+
const tl = startNum <= endNum ? startRaw : endRaw;
|
|
734
|
+
const br = startNum <= endNum ? endRaw : startRaw;
|
|
735
|
+
return {
|
|
736
|
+
kind: "col",
|
|
737
|
+
ooxml: `$${tl}:$${br}`,
|
|
738
|
+
dimensions: `${tl}:${br}`
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
return undefined;
|
|
742
|
+
}
|
|
743
|
+
/**
|
|
744
|
+
* Normalise a user-supplied `printArea` value into the canonical OOXML
|
|
745
|
+
* `'Sheet'!<ref>` form. Returns `undefined` for malformed input so the
|
|
746
|
+
* caller drops the entry instead of emitting corrupt XML.
|
|
747
|
+
*
|
|
748
|
+
* `printArea` accepts cell, range, whole-row, and whole-column shapes —
|
|
749
|
+
* Excel itself supports all four (e.g. selecting entire columns A:C as
|
|
750
|
+
* the print area is a common UI gesture). Bare cell inputs are promoted
|
|
751
|
+
* to a degenerate range `$A$1:$A$1` because that is what Excel itself
|
|
752
|
+
* emits for a single-cell print area, and the worksheet API exposes
|
|
753
|
+
* `printArea` as a range string (single-cell entries surface as `A1:A1`).
|
|
754
|
+
*/
|
|
755
|
+
function normalisePrintAreaRange(input, sheetName) {
|
|
756
|
+
const ref = parsePrintReference(input);
|
|
757
|
+
if (!ref) {
|
|
758
|
+
return undefined;
|
|
759
|
+
}
|
|
760
|
+
const ooxml = ref.kind === "cell" ? `${ref.ooxml}:${ref.ooxml}` : ref.ooxml;
|
|
761
|
+
return `${quoteSheetName(sheetName)}!${ooxml}`;
|
|
762
|
+
}
|
|
763
|
+
/**
|
|
764
|
+
* Normalise a user-supplied print-titles row or column expression into
|
|
765
|
+
* the canonical OOXML form `'Sheet'!$N:$N` (rows) or `'Sheet'!$L:$L`
|
|
766
|
+
* (columns).
|
|
767
|
+
*
|
|
768
|
+
* Long-standing excelts behaviour lets users put a column expression on
|
|
769
|
+
* `printTitlesRow` (and vice versa) — the OOXML reader has always
|
|
770
|
+
* re-classified the value onto the correct field on round-trip — so we
|
|
771
|
+
* honour that by letting the parser infer the actual axis from the
|
|
772
|
+
* input shape. Strict enforcement would silently drop print titles
|
|
773
|
+
* users have set successfully for years.
|
|
774
|
+
*/
|
|
775
|
+
function normalisePrintTitlesAxis(input, sheetName) {
|
|
776
|
+
const ref = parsePrintReference(input);
|
|
777
|
+
if (!ref || (ref.kind !== "row" && ref.kind !== "col")) {
|
|
778
|
+
return undefined;
|
|
779
|
+
}
|
|
780
|
+
return `${quoteSheetName(sheetName)}!${ref.ooxml}`;
|
|
781
|
+
}
|
|
405
782
|
export { WorkbookXform };
|
|
@@ -54,7 +54,15 @@ const colCache = {
|
|
|
54
54
|
let l3;
|
|
55
55
|
let n = 1;
|
|
56
56
|
if (level >= 4) {
|
|
57
|
-
|
|
57
|
+
// Defensive invariant: Excel's column space (XFD = 16,384) caps at
|
|
58
|
+
// three letters, so neither `l2n` nor `n2l` should ever ask for a
|
|
59
|
+
// higher level. Both callers validate before reaching here; if
|
|
60
|
+
// this branch fires it indicates a programming error in a future
|
|
61
|
+
// caller, not a user input problem — surface that clearly rather
|
|
62
|
+
// than reusing `ColumnOutOfBoundsError` (which would lie about
|
|
63
|
+
// the offending column, since `level` is a letter-count, not a
|
|
64
|
+
// column number).
|
|
65
|
+
throw new Error(`colCache._fill: invariant violated — level ${level} exceeds the 3-letter cap; callers must validate before invoking _fill`);
|
|
58
66
|
}
|
|
59
67
|
if (this._l2nFill < 1 && level >= 1) {
|
|
60
68
|
while (n <= 26) {
|
|
@@ -95,6 +103,16 @@ const colCache = {
|
|
|
95
103
|
},
|
|
96
104
|
l2n(l) {
|
|
97
105
|
if (!this._l2n[l]) {
|
|
106
|
+
// Excel's column space stops at XFD (16,384) — three letters is
|
|
107
|
+
// the maximum width any valid column letter can have. Reject
|
|
108
|
+
// longer inputs explicitly here, BEFORE handing the length to
|
|
109
|
+
// `_fill`, so the thrown error carries the actual offending
|
|
110
|
+
// letter (`AAAA`) rather than the level integer (`4`) — matching
|
|
111
|
+
// what the equivalent `n2l(n > 16384)` and `decodeAddress` paths
|
|
112
|
+
// already report.
|
|
113
|
+
if (l.length > 3) {
|
|
114
|
+
throw new errors_1.ColumnOutOfBoundsError(l, "Excel supports columns from 1 to 16384");
|
|
115
|
+
}
|
|
98
116
|
this._fill(l.length);
|
|
99
117
|
}
|
|
100
118
|
if (!this._l2n[l]) {
|