@rowsncolumns/toolkit 24.5.0 → 24.5.3

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.
@@ -2,15 +2,17 @@ import type { GridRange } from "@rowsncolumns/common-types";
2
2
  import type { Column, FilterSpec, SortSpecs, TableTotalsRowFunction } from "@rowsncolumns/spreadsheet";
3
3
  export declare const getRangeFromAddressRef: (ref: string) => GridRange | undefined;
4
4
  /**
5
- * Convert an xlsx column width (in units of max-digit-width of the
6
- * default workbook font, Calibri 11 → MDW=7px) to pixels.
5
+ * OOXML `<col width>` (and `<sheetFormatPr defaultColWidth>`) → pixels.
7
6
  *
8
- * Excel's effective rendered pixel count includes a 5-pixel padding
9
- * baked into the column box: `pixels = floor(width × MDW + padding)`.
10
- * The default width "8.43" renders as 64 px (8.43 × 7 + 5 ≈ 64.01),
11
- * matching Excel exactly. The "pure" ECMA-376 §18.3.1.13 formula omits
12
- * the 5px padding; we keep the +5 form so our pixel output matches
13
- * Excel's rendered widths and round-trips with our exporter.
7
+ * The stored width is already in *padded* character units: Excel writes
8
+ * `Truncate((chars × MDW + 5) / MDW × 256) / 256` — the default 8.43-character
9
+ * column is stored as 9.140625 — and renders it back as
10
+ * `Truncate(((256 × width + Truncate(128 / MDW)) / 256) × MDW)`
11
+ * (ECMA-376 §18.3.1.13), i.e. 9.140625 → 64 px. Adding the 5 px padding a
12
+ * second time (the previous `floor(width × 7 + 5)`) drew every Excel-authored
13
+ * column ~5 px wider than Excel does and grew widths by 0.71 characters per
14
+ * import/export cycle (the exporter stores `px / 7`). MDW is 7 px for the
15
+ * Calibri 11 / Aptos Narrow 12 defaults at 96 dpi.
14
16
  */
15
17
  export declare function excelColumnWidthToPixels(width: number): number;
16
18
  export declare function findMax(arr: number[]): number;
@@ -1 +1 @@
1
- {"version":3,"file":"helpers.d.ts","sourceRoot":"","sources":["../../../excel-parser/helpers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAC;AAC5D,OAAO,KAAK,EAEV,MAAM,EACN,UAAU,EACV,SAAS,EACT,sBAAsB,EACvB,MAAM,2BAA2B,CAAC;AAUnC,eAAO,MAAM,sBAAsB,GAAI,KAAK,MAAM,KAAG,SAAS,GAAG,SAIhE,CAAC;AAEF;;;;;;;;;;GAUG;AACH,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,MAAM,UAIrD;AAED,wBAAgB,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,UAMpC;AAmBD,eAAO,MAAM,wBAAwB,GACnC,OAAO,OAAO,KACb,KAAK,IAAI,sBACwD,CAAC;AAErE;;;;;GAKG;AACH,eAAO,MAAM,sBAAsB,GACjC,QAAQ,GAAG,KACV,IAAI,CAAC,MAAM,EAAE,mBAAmB,GAAG,gBAAgB,GAAG,kBAAkB,CAmB1E,CAAC;AAqBF,wBAAgB,+BAA+B,CAC7C,UAAU,EAAE,GAAG;AACf;;;;GAIG;AACH,eAAe,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,OAAO,KAAK,MAAM,GAAG,SAAS,gBA0G5E;AAED;;;;GAIG;AACH,wBAAgB,yBAAyB,CAAC,UAAU,EAAE,GAAG,GAAG,GAAG,CAAC,MAAM,CAAC,CAgBtE;AAED;;;;;;GAMG;AACH,wBAAgB,4BAA4B,CAC1C,SAAS,EAAE,GAAG,EACd,KAAK,EAAE,SAAS,EAChB,eAAe,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,OAAO,KAAK,MAAM,GAAG,SAAS,GAC1E,SAAS,EAAE,CA2Cb;AAED;;GAEG;AACH,wBAAsB,aAAa,CAAC,IAAI,EAAE,IAAI,GAAG,WAAW,wBAkB3D"}
1
+ {"version":3,"file":"helpers.d.ts","sourceRoot":"","sources":["../../../excel-parser/helpers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAC;AAC5D,OAAO,KAAK,EAEV,MAAM,EACN,UAAU,EACV,SAAS,EACT,sBAAsB,EACvB,MAAM,2BAA2B,CAAC;AAUnC,eAAO,MAAM,sBAAsB,GAAI,KAAK,MAAM,KAAG,SAAS,GAAG,SAIhE,CAAC;AAEF;;;;;;;;;;;;GAYG;AACH,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,MAAM,UAMrD;AAED,wBAAgB,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,UAMpC;AAmBD,eAAO,MAAM,wBAAwB,GACnC,OAAO,OAAO,KACb,KAAK,IAAI,sBACwD,CAAC;AAErE;;;;;GAKG;AACH,eAAO,MAAM,sBAAsB,GACjC,QAAQ,GAAG,KACV,IAAI,CAAC,MAAM,EAAE,mBAAmB,GAAG,gBAAgB,GAAG,kBAAkB,CAmB1E,CAAC;AAqBF,wBAAgB,+BAA+B,CAC7C,UAAU,EAAE,GAAG;AACf;;;;GAIG;AACH,eAAe,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,OAAO,KAAK,MAAM,GAAG,SAAS,gBA0G5E;AAED;;;;GAIG;AACH,wBAAgB,yBAAyB,CAAC,UAAU,EAAE,GAAG,GAAG,GAAG,CAAC,MAAM,CAAC,CAgBtE;AAED;;;;;;GAMG;AACH,wBAAgB,4BAA4B,CAC1C,SAAS,EAAE,GAAG,EACd,KAAK,EAAE,SAAS,EAChB,eAAe,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,OAAO,KAAK,MAAM,GAAG,SAAS,GAC1E,SAAS,EAAE,CA2Cb;AAED;;GAEG;AACH,wBAAsB,aAAa,CAAC,IAAI,EAAE,IAAI,GAAG,WAAW,wBAkB3D"}
@@ -8,20 +8,23 @@ export var getRangeFromAddressRef = function (ref) {
8
8
  return selection.range;
9
9
  };
10
10
  /**
11
- * Convert an xlsx column width (in units of max-digit-width of the
12
- * default workbook font, Calibri 11 → MDW=7px) to pixels.
11
+ * OOXML `<col width>` (and `<sheetFormatPr defaultColWidth>`) → pixels.
13
12
  *
14
- * Excel's effective rendered pixel count includes a 5-pixel padding
15
- * baked into the column box: `pixels = floor(width × MDW + padding)`.
16
- * The default width "8.43" renders as 64 px (8.43 × 7 + 5 ≈ 64.01),
17
- * matching Excel exactly. The "pure" ECMA-376 §18.3.1.13 formula omits
18
- * the 5px padding; we keep the +5 form so our pixel output matches
19
- * Excel's rendered widths and round-trips with our exporter.
13
+ * The stored width is already in *padded* character units: Excel writes
14
+ * `Truncate((chars × MDW + 5) / MDW × 256) / 256` — the default 8.43-character
15
+ * column is stored as 9.140625 — and renders it back as
16
+ * `Truncate(((256 × width + Truncate(128 / MDW)) / 256) × MDW)`
17
+ * (ECMA-376 §18.3.1.13), i.e. 9.140625 → 64 px. Adding the 5 px padding a
18
+ * second time (the previous `floor(width × 7 + 5)`) drew every Excel-authored
19
+ * column ~5 px wider than Excel does and grew widths by 0.71 characters per
20
+ * import/export cycle (the exporter stores `px / 7`). MDW is 7 px for the
21
+ * Calibri 11 / Aptos Narrow 12 defaults at 96 dpi.
20
22
  */
21
23
  export function excelColumnWidthToPixels(width) {
22
- var padding = 5;
23
- var charWidth = 7;
24
- return Math.floor(width * charWidth + padding);
24
+ var maxDigitWidth = 7;
25
+ if (!Number.isFinite(width) || width <= 0)
26
+ return 0;
27
+ return Math.trunc(((256 * width + Math.trunc(128 / maxDigitWidth)) / 256) * maxDigitWidth);
25
28
  }
26
29
  export function findMax(arr) {
27
30
  var max = arr[0];
@@ -1444,8 +1444,113 @@ var normalizeMergeRanges = function (ranges) {
1444
1444
  }
1445
1445
  return valid;
1446
1446
  };
1447
+ /** Escape `"` as `""` inside a formula string literal. */
1448
+ var escapeFormulaText = function (value) { return value.replace(/"/g, '""'); };
1449
+ // `xlsxSource` is meant to hold the verbatim OOXML representation, but
1450
+ // writers other than the importer have filled it from Excel's JavaScript-API
1451
+ // vocabulary: a bare `list.source` literal (`On track,Watch,Off track`),
1452
+ // PascalCase operators (`Between`), PascalCase types (`WholeNumber`) and
1453
+ // `=`-prefixed formulas. Excel opens a file carrying any of those in repair
1454
+ // mode and drops the rule, so the exporter normalizes every field before
1455
+ // replaying it, and a type / operator that is not an OOXML token even after
1456
+ // normalization (`undefined` below) is never replayed — the rule falls
1457
+ // through to the condition synthesizer. Twin of `normalize_source_*` in
1458
+ // `rust/crates/rnc-export/src/xlsx/datavalidation.rs` — keep the two in sync
1459
+ // so both engines emit identical XML.
1460
+ var normalizeXlsxSourceType = function (type) {
1461
+ switch (type.trim().toLowerCase()) {
1462
+ case "list":
1463
+ return "list";
1464
+ case "whole":
1465
+ case "wholenumber":
1466
+ return "whole";
1467
+ case "decimal":
1468
+ return "decimal";
1469
+ case "date":
1470
+ return "date";
1471
+ case "time":
1472
+ return "time";
1473
+ case "textlength":
1474
+ return "textLength";
1475
+ case "custom":
1476
+ return "custom";
1477
+ default:
1478
+ return undefined;
1479
+ }
1480
+ };
1481
+ var normalizeXlsxSourceOperator = function (operator) {
1482
+ switch (operator.trim().toLowerCase()) {
1483
+ case "between":
1484
+ return "between";
1485
+ case "notbetween":
1486
+ return "notBetween";
1487
+ case "equal":
1488
+ case "equalto":
1489
+ return "equal";
1490
+ case "notequal":
1491
+ case "notequalto":
1492
+ return "notEqual";
1493
+ case "greaterthan":
1494
+ return "greaterThan";
1495
+ case "lessthan":
1496
+ return "lessThan";
1497
+ case "greaterthanorequal":
1498
+ case "greaterthanorequalto":
1499
+ return "greaterThanOrEqual";
1500
+ case "lessthanorequal":
1501
+ case "lessthanorequalto":
1502
+ return "lessThanOrEqual";
1503
+ default:
1504
+ return undefined;
1505
+ }
1506
+ };
1507
+ // OOXML `<formula1>` text never carries the leading `=` a formula-style
1508
+ // source (`=Sheet1!$A$1:$A$5`) has.
1509
+ var stripLeadingEquals = function (formula) {
1510
+ var trimmed = formula.trim();
1511
+ return trimmed.startsWith("=") ? trimmed.slice(1) : trimmed;
1512
+ };
1513
+ // One identifier token — `[A-Za-z_\\][A-Za-z0-9_.]*` — which is what a
1514
+ // defined name (`StatusList`, `My_List.v2`) or a single A1 cell (`A1`) looks
1515
+ // like; a word list with commas or spaces is not one.
1516
+ var XLSX_IDENTIFIER_TOKEN = /^[A-Za-z_\\][A-Za-z0-9_.]*$/;
1517
+ // A single A1 cell token: 1–3 letters followed by digits.
1518
+ var XLSX_A1_CELL_TOKEN = /^[A-Za-z]{1,3}[0-9]+$/;
1519
+ // A function call — `OFFSET(…)`, `INDIRECT(…)` — which Excel writes without
1520
+ // a leading `=`.
1521
+ var XLSX_FUNCTION_CALL = /^[A-Za-z_\\][A-Za-z0-9_.]*\(.*\)$/;
1522
+ // `true` when a list source reads as a reference / formula rather than as
1523
+ // list text: it carries a reference marker (`!` sheet qualifier, `:` range,
1524
+ // `$` absolute anchor, `[` structured / external ref), is a single A1 cell
1525
+ // or defined-name token, or is a function call. Anything else — comma lists,
1526
+ // text with spaces, digit-only entries — is a bare literal that Excel only
1527
+ // accepts quoted.
1528
+ var looksLikeXlsxListReference = function (text) {
1529
+ return text.length > 0 &&
1530
+ (/[!:$[]/.test(text) ||
1531
+ XLSX_A1_CELL_TOKEN.test(text) ||
1532
+ XLSX_IDENTIFIER_TOKEN.test(text) ||
1533
+ XLSX_FUNCTION_CALL.test(text));
1534
+ };
1535
+ // A `list` source is valid OOXML only as a quoted string literal (`"a,b"`)
1536
+ // or a reference (`$A$1:$A$5`, `Sheet1!$A$1:$A$5`, `StatusList`, `OFFSET(…)`).
1537
+ // Excel's API hands back a literal list as bare comma-separated text and a
1538
+ // reference with a leading `=`, so: quoted → verbatim, `=`-prefixed → the `=`
1539
+ // is stripped and the rest replayed verbatim, reference-like → verbatim,
1540
+ // anything else → a literal that must be quoted (with `"` doubled inside, as
1541
+ // the ONE_OF_LIST branch does).
1542
+ var normalizeXlsxSourceListFormula1 = function (formula1) {
1543
+ var trimmed = formula1.trim();
1544
+ if (trimmed.startsWith('"'))
1545
+ return trimmed;
1546
+ if (trimmed.startsWith("="))
1547
+ return trimmed.slice(1).trim();
1548
+ if (looksLikeXlsxListReference(trimmed))
1549
+ return trimmed;
1550
+ return "\"".concat(escapeFormulaText(trimmed), "\"");
1551
+ };
1447
1552
  var convertDataValidationRule = function (rule) {
1448
- var _a, _b, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _s, _t, _u, _v, _w, _x, _y, _z, _0, _1, _2, _3, _4, _5;
1553
+ var _a, _b, _d, _e, _f, _g;
1449
1554
  if (!rule)
1450
1555
  return undefined;
1451
1556
  var allowBlank = rule.allowBlank !== false;
@@ -1462,14 +1567,48 @@ var convertDataValidationRule = function (rule) {
1462
1567
  prompt: (_g = inputMessage === null || inputMessage === void 0 ? void 0 : inputMessage.message) !== null && _g !== void 0 ? _g : undefined,
1463
1568
  };
1464
1569
  // Round-trip path: when the rule was imported from xlsx, replay the
1465
- // verbatim source representation. Preserves type/operator/formulas
1466
- // exactly, including `whole`/`textLength`/`time` types and user-written
1467
- // custom formulas without `INDIRECT("RC",FALSE)` rewriting.
1570
+ // source representation after normalizing it to OOXML vocabulary (see
1571
+ // `normalizeXlsxSource*`). Preserves type/operator/formulas, including
1572
+ // `whole`/`textLength`/`time` types and user-written custom formulas
1573
+ // without `INDIRECT("RC",FALSE)` rewriting. A type / operator that is not
1574
+ // an OOXML token even after normalization (Excel's `Inconsistent`,
1575
+ // `MixedCriteria`) is never written: the rule falls through to the
1576
+ // condition synthesizer.
1468
1577
  var src = rule.xlsxSource;
1578
+ var rejectedListSource;
1469
1579
  if (src && src.type && typeof src.formula1 === "string" && src.formula1) {
1470
- return __assign({ type: src.type, operator: src.operator, formula1: src.formula1, formula2: src.formula2, showDropDown: src.type === "list" && rule.displayStyle === "plain" ? true : undefined }, baseConfig);
1580
+ var type = normalizeXlsxSourceType(src.type);
1581
+ var operator = src.operator
1582
+ ? normalizeXlsxSourceOperator(src.operator)
1583
+ : undefined;
1584
+ var operatorValid = !src.operator || operator !== undefined;
1585
+ if (type !== undefined && operatorValid) {
1586
+ var formula1 = type === "list"
1587
+ ? normalizeXlsxSourceListFormula1(src.formula1)
1588
+ : stripLeadingEquals(src.formula1);
1589
+ return __assign({ type: type, operator: operator, formula1: formula1, formula2: src.formula2 ? stripLeadingEquals(src.formula2) : undefined, showDropDown: type === "list" && rule.displayStyle === "plain" ? true : undefined }, baseConfig);
1590
+ }
1591
+ if (type === "list") {
1592
+ rejectedListSource = normalizeXlsxSourceListFormula1(src.formula1);
1593
+ }
1594
+ }
1595
+ var synthesized = convertDataValidationCondition(rule, baseConfig);
1596
+ if (synthesized)
1597
+ return synthesized;
1598
+ // A rejected `list` source with no usable condition is still kept — its
1599
+ // normalized source text (quoted literal or reference) is always valid
1600
+ // OOXML. Anything else rejected has nothing valid to write and is skipped.
1601
+ if (rejectedListSource !== undefined) {
1602
+ return __assign({ type: "list", formula1: rejectedListSource, showDropDown: rule.displayStyle === "plain" ? true : undefined }, baseConfig);
1471
1603
  }
1472
- if (!((_h = rule.condition) === null || _h === void 0 ? void 0 : _h.type)) {
1604
+ return undefined;
1605
+ };
1606
+ // The condition-based synthesizer: the Excel config for a rule that has no
1607
+ // replayable `xlsxSource` (UI-authored, edited after import, or rejected by
1608
+ // the round-trip guard above).
1609
+ var convertDataValidationCondition = function (rule, baseConfig) {
1610
+ var _a, _b, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _s, _t, _u, _v, _w, _x, _y, _z;
1611
+ if (!((_a = rule.condition) === null || _a === void 0 ? void 0 : _a.type)) {
1473
1612
  // Prompt-only DV (no validation rule, just an input-message tooltip
1474
1613
  // and/or an error alert). Emit it with no type/formula so Excel still
1475
1614
  // shows the tooltip when the cell is selected.
@@ -1479,9 +1618,8 @@ var convertDataValidationRule = function (rule) {
1479
1618
  return undefined;
1480
1619
  }
1481
1620
  var condition = rule.condition;
1482
- var values = (_j = condition.values) !== null && _j !== void 0 ? _j : [];
1621
+ var values = (_b = condition.values) !== null && _b !== void 0 ? _b : [];
1483
1622
  var currentCell = 'INDIRECT("RC",FALSE)';
1484
- var escapeFormulaText = function (value) { return value.replace(/"/g, '""'); };
1485
1623
  var ensureFormula = function (value) {
1486
1624
  return value.startsWith("=") ? value : "=".concat(value);
1487
1625
  };
@@ -1505,7 +1643,7 @@ var convertDataValidationRule = function (rule) {
1505
1643
  return __assign({ type: "list", formula1: formula1.startsWith("=") ? formula1 : "=".concat(formula1), showDropDown: rule.displayStyle === "plain" ? true : undefined }, baseConfig);
1506
1644
  }
1507
1645
  if (condition.type === "CUSTOM_FORMULA") {
1508
- var raw = (_l = (_k = values[0]) === null || _k === void 0 ? void 0 : _k.userEnteredValue) !== null && _l !== void 0 ? _l : "";
1646
+ var raw = (_e = (_d = values[0]) === null || _d === void 0 ? void 0 : _d.userEnteredValue) !== null && _e !== void 0 ? _e : "";
1509
1647
  if (!raw)
1510
1648
  return undefined;
1511
1649
  return __assign({ type: "custom", formula1: ensureFormula(raw) }, baseConfig);
@@ -1520,33 +1658,33 @@ var convertDataValidationRule = function (rule) {
1520
1658
  return __assign({ type: "custom", formula1: "=".concat(currentCell, "<>\"\"") }, baseConfig);
1521
1659
  }
1522
1660
  if (condition.type === "TEXT_EQ" || condition.type === "TEXT_NOT_EQ") {
1523
- var raw = (_o = (_m = values[0]) === null || _m === void 0 ? void 0 : _m.userEnteredValue) !== null && _o !== void 0 ? _o : "";
1661
+ var raw = (_g = (_f = values[0]) === null || _f === void 0 ? void 0 : _f.userEnteredValue) !== null && _g !== void 0 ? _g : "";
1524
1662
  if (!raw)
1525
1663
  return undefined;
1526
1664
  var op = condition.type === "TEXT_EQ" ? "=" : "<>";
1527
1665
  return __assign({ type: "custom", formula1: "=".concat(currentCell).concat(op, "\"").concat(escapeFormulaText(raw), "\"") }, baseConfig);
1528
1666
  }
1529
1667
  if (condition.type === "TEXT_CONTAINS") {
1530
- var raw = (_q = (_p = values[0]) === null || _p === void 0 ? void 0 : _p.userEnteredValue) !== null && _q !== void 0 ? _q : "";
1668
+ var raw = (_j = (_h = values[0]) === null || _h === void 0 ? void 0 : _h.userEnteredValue) !== null && _j !== void 0 ? _j : "";
1531
1669
  if (!raw)
1532
1670
  return undefined;
1533
1671
  return __assign({ type: "custom", formula1: "=ISNUMBER(SEARCH(\"".concat(escapeFormulaText(raw), "\",").concat(currentCell, "))") }, baseConfig);
1534
1672
  }
1535
1673
  if (condition.type === "TEXT_NOT_CONTAINS") {
1536
- var raw = (_t = (_s = values[0]) === null || _s === void 0 ? void 0 : _s.userEnteredValue) !== null && _t !== void 0 ? _t : "";
1674
+ var raw = (_l = (_k = values[0]) === null || _k === void 0 ? void 0 : _k.userEnteredValue) !== null && _l !== void 0 ? _l : "";
1537
1675
  if (!raw)
1538
1676
  return undefined;
1539
1677
  return __assign({ type: "custom", formula1: "=ISERROR(SEARCH(\"".concat(escapeFormulaText(raw), "\",").concat(currentCell, "))") }, baseConfig);
1540
1678
  }
1541
1679
  if (condition.type === "TEXT_STARTS_WITH") {
1542
- var raw = (_v = (_u = values[0]) === null || _u === void 0 ? void 0 : _u.userEnteredValue) !== null && _v !== void 0 ? _v : "";
1680
+ var raw = (_o = (_m = values[0]) === null || _m === void 0 ? void 0 : _m.userEnteredValue) !== null && _o !== void 0 ? _o : "";
1543
1681
  if (!raw)
1544
1682
  return undefined;
1545
1683
  var text = escapeFormulaText(raw);
1546
1684
  return __assign({ type: "custom", formula1: "=LEFT(".concat(currentCell, ",LEN(\"").concat(text, "\"))=\"").concat(text, "\"") }, baseConfig);
1547
1685
  }
1548
1686
  if (condition.type === "TEXT_ENDS_WITH") {
1549
- var raw = (_x = (_w = values[0]) === null || _w === void 0 ? void 0 : _w.userEnteredValue) !== null && _x !== void 0 ? _x : "";
1687
+ var raw = (_q = (_p = values[0]) === null || _p === void 0 ? void 0 : _p.userEnteredValue) !== null && _q !== void 0 ? _q : "";
1550
1688
  if (!raw)
1551
1689
  return undefined;
1552
1690
  var text = escapeFormulaText(raw);
@@ -1566,10 +1704,10 @@ var convertDataValidationRule = function (rule) {
1566
1704
  condition.type === "NUMBER_GREATER_THAN_EQ" ||
1567
1705
  condition.type === "NUMBER_LESS" ||
1568
1706
  condition.type === "NUMBER_LESS_THAN_EQ") {
1569
- var first = (_z = (_y = values[0]) === null || _y === void 0 ? void 0 : _y.userEnteredValue) !== null && _z !== void 0 ? _z : "";
1707
+ var first = (_t = (_s = values[0]) === null || _s === void 0 ? void 0 : _s.userEnteredValue) !== null && _t !== void 0 ? _t : "";
1570
1708
  if (!first)
1571
1709
  return undefined;
1572
- var second = (_1 = (_0 = values[1]) === null || _0 === void 0 ? void 0 : _0.userEnteredValue) !== null && _1 !== void 0 ? _1 : "";
1710
+ var second = (_v = (_u = values[1]) === null || _u === void 0 ? void 0 : _u.userEnteredValue) !== null && _v !== void 0 ? _v : "";
1573
1711
  var operatorMap = {
1574
1712
  NUMBER_BETWEEN: "between",
1575
1713
  NUMBER_NOT_BETWEEN: "notBetween",
@@ -1606,10 +1744,10 @@ var convertDataValidationRule = function (rule) {
1606
1744
  condition.type === "DATE_ON_OR_AFTER" ||
1607
1745
  condition.type === "DATE_BETWEEN" ||
1608
1746
  condition.type === "DATE_NOT_BETWEEN") {
1609
- var first = (_3 = (_2 = values[0]) === null || _2 === void 0 ? void 0 : _2.userEnteredValue) !== null && _3 !== void 0 ? _3 : "";
1747
+ var first = (_x = (_w = values[0]) === null || _w === void 0 ? void 0 : _w.userEnteredValue) !== null && _x !== void 0 ? _x : "";
1610
1748
  if (!first)
1611
1749
  return undefined;
1612
- var second = (_5 = (_4 = values[1]) === null || _4 === void 0 ? void 0 : _4.userEnteredValue) !== null && _5 !== void 0 ? _5 : "";
1750
+ var second = (_z = (_y = values[1]) === null || _y === void 0 ? void 0 : _y.userEnteredValue) !== null && _z !== void 0 ? _z : "";
1613
1751
  var operatorMap = {
1614
1752
  DATE_EQ: "equal",
1615
1753
  DATE_NOT_EQ: "notEqual",
@@ -2,15 +2,17 @@ import type { GridRange } from "@rowsncolumns/common-types";
2
2
  import type { Column, FilterSpec, SortSpecs, TableTotalsRowFunction } from "@rowsncolumns/spreadsheet";
3
3
  export declare const getRangeFromAddressRef: (ref: string) => GridRange | undefined;
4
4
  /**
5
- * Convert an xlsx column width (in units of max-digit-width of the
6
- * default workbook font, Calibri 11 → MDW=7px) to pixels.
5
+ * OOXML `<col width>` (and `<sheetFormatPr defaultColWidth>`) → pixels.
7
6
  *
8
- * Excel's effective rendered pixel count includes a 5-pixel padding
9
- * baked into the column box: `pixels = floor(width × MDW + padding)`.
10
- * The default width "8.43" renders as 64 px (8.43 × 7 + 5 ≈ 64.01),
11
- * matching Excel exactly. The "pure" ECMA-376 §18.3.1.13 formula omits
12
- * the 5px padding; we keep the +5 form so our pixel output matches
13
- * Excel's rendered widths and round-trips with our exporter.
7
+ * The stored width is already in *padded* character units: Excel writes
8
+ * `Truncate((chars × MDW + 5) / MDW × 256) / 256` — the default 8.43-character
9
+ * column is stored as 9.140625 — and renders it back as
10
+ * `Truncate(((256 × width + Truncate(128 / MDW)) / 256) × MDW)`
11
+ * (ECMA-376 §18.3.1.13), i.e. 9.140625 → 64 px. Adding the 5 px padding a
12
+ * second time (the previous `floor(width × 7 + 5)`) drew every Excel-authored
13
+ * column ~5 px wider than Excel does and grew widths by 0.71 characters per
14
+ * import/export cycle (the exporter stores `px / 7`). MDW is 7 px for the
15
+ * Calibri 11 / Aptos Narrow 12 defaults at 96 dpi.
14
16
  */
15
17
  export declare function excelColumnWidthToPixels(width: number): number;
16
18
  export declare function findMax(arr: number[]): number;
@@ -1 +1 @@
1
- {"version":3,"file":"helpers.d.ts","sourceRoot":"","sources":["../../../excel-parser/helpers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAC;AAC5D,OAAO,KAAK,EAEV,MAAM,EACN,UAAU,EACV,SAAS,EACT,sBAAsB,EACvB,MAAM,2BAA2B,CAAC;AAUnC,eAAO,MAAM,sBAAsB,GAAI,KAAK,MAAM,KAAG,SAAS,GAAG,SAIhE,CAAC;AAEF;;;;;;;;;;GAUG;AACH,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,MAAM,UAIrD;AAED,wBAAgB,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,UAMpC;AAmBD,eAAO,MAAM,wBAAwB,GACnC,OAAO,OAAO,KACb,KAAK,IAAI,sBACwD,CAAC;AAErE;;;;;GAKG;AACH,eAAO,MAAM,sBAAsB,GACjC,QAAQ,GAAG,KACV,IAAI,CAAC,MAAM,EAAE,mBAAmB,GAAG,gBAAgB,GAAG,kBAAkB,CAmB1E,CAAC;AAqBF,wBAAgB,+BAA+B,CAC7C,UAAU,EAAE,GAAG;AACf;;;;GAIG;AACH,eAAe,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,OAAO,KAAK,MAAM,GAAG,SAAS,gBA0G5E;AAED;;;;GAIG;AACH,wBAAgB,yBAAyB,CAAC,UAAU,EAAE,GAAG,GAAG,GAAG,CAAC,MAAM,CAAC,CAgBtE;AAED;;;;;;GAMG;AACH,wBAAgB,4BAA4B,CAC1C,SAAS,EAAE,GAAG,EACd,KAAK,EAAE,SAAS,EAChB,eAAe,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,OAAO,KAAK,MAAM,GAAG,SAAS,GAC1E,SAAS,EAAE,CA2Cb;AAED;;GAEG;AACH,wBAAsB,aAAa,CAAC,IAAI,EAAE,IAAI,GAAG,WAAW,wBAkB3D"}
1
+ {"version":3,"file":"helpers.d.ts","sourceRoot":"","sources":["../../../excel-parser/helpers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAC;AAC5D,OAAO,KAAK,EAEV,MAAM,EACN,UAAU,EACV,SAAS,EACT,sBAAsB,EACvB,MAAM,2BAA2B,CAAC;AAUnC,eAAO,MAAM,sBAAsB,GAAI,KAAK,MAAM,KAAG,SAAS,GAAG,SAIhE,CAAC;AAEF;;;;;;;;;;;;GAYG;AACH,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,MAAM,UAMrD;AAED,wBAAgB,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,UAMpC;AAmBD,eAAO,MAAM,wBAAwB,GACnC,OAAO,OAAO,KACb,KAAK,IAAI,sBACwD,CAAC;AAErE;;;;;GAKG;AACH,eAAO,MAAM,sBAAsB,GACjC,QAAQ,GAAG,KACV,IAAI,CAAC,MAAM,EAAE,mBAAmB,GAAG,gBAAgB,GAAG,kBAAkB,CAmB1E,CAAC;AAqBF,wBAAgB,+BAA+B,CAC7C,UAAU,EAAE,GAAG;AACf;;;;GAIG;AACH,eAAe,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,OAAO,KAAK,MAAM,GAAG,SAAS,gBA0G5E;AAED;;;;GAIG;AACH,wBAAgB,yBAAyB,CAAC,UAAU,EAAE,GAAG,GAAG,GAAG,CAAC,MAAM,CAAC,CAgBtE;AAED;;;;;;GAMG;AACH,wBAAgB,4BAA4B,CAC1C,SAAS,EAAE,GAAG,EACd,KAAK,EAAE,SAAS,EAChB,eAAe,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,OAAO,KAAK,MAAM,GAAG,SAAS,GAC1E,SAAS,EAAE,CA2Cb;AAED;;GAEG;AACH,wBAAsB,aAAa,CAAC,IAAI,EAAE,IAAI,GAAG,WAAW,wBAkB3D"}
@@ -7,20 +7,23 @@ export const getRangeFromAddressRef = (ref) => {
7
7
  return selection.range;
8
8
  };
9
9
  /**
10
- * Convert an xlsx column width (in units of max-digit-width of the
11
- * default workbook font, Calibri 11 → MDW=7px) to pixels.
10
+ * OOXML `<col width>` (and `<sheetFormatPr defaultColWidth>`) → pixels.
12
11
  *
13
- * Excel's effective rendered pixel count includes a 5-pixel padding
14
- * baked into the column box: `pixels = floor(width × MDW + padding)`.
15
- * The default width "8.43" renders as 64 px (8.43 × 7 + 5 ≈ 64.01),
16
- * matching Excel exactly. The "pure" ECMA-376 §18.3.1.13 formula omits
17
- * the 5px padding; we keep the +5 form so our pixel output matches
18
- * Excel's rendered widths and round-trips with our exporter.
12
+ * The stored width is already in *padded* character units: Excel writes
13
+ * `Truncate((chars × MDW + 5) / MDW × 256) / 256` — the default 8.43-character
14
+ * column is stored as 9.140625 — and renders it back as
15
+ * `Truncate(((256 × width + Truncate(128 / MDW)) / 256) × MDW)`
16
+ * (ECMA-376 §18.3.1.13), i.e. 9.140625 → 64 px. Adding the 5 px padding a
17
+ * second time (the previous `floor(width × 7 + 5)`) drew every Excel-authored
18
+ * column ~5 px wider than Excel does and grew widths by 0.71 characters per
19
+ * import/export cycle (the exporter stores `px / 7`). MDW is 7 px for the
20
+ * Calibri 11 / Aptos Narrow 12 defaults at 96 dpi.
19
21
  */
20
22
  export function excelColumnWidthToPixels(width) {
21
- const padding = 5;
22
- const charWidth = 7;
23
- return Math.floor(width * charWidth + padding);
23
+ const maxDigitWidth = 7;
24
+ if (!Number.isFinite(width) || width <= 0)
25
+ return 0;
26
+ return Math.trunc(((256 * width + Math.trunc(128 / maxDigitWidth)) / 256) * maxDigitWidth);
24
27
  }
25
28
  export function findMax(arr) {
26
29
  let max = arr[0];
@@ -1086,6 +1086,109 @@ const normalizeMergeRanges = (ranges) => {
1086
1086
  }
1087
1087
  return valid;
1088
1088
  };
1089
+ /** Escape `"` as `""` inside a formula string literal. */
1090
+ const escapeFormulaText = (value) => value.replace(/"/g, '""');
1091
+ // `xlsxSource` is meant to hold the verbatim OOXML representation, but
1092
+ // writers other than the importer have filled it from Excel's JavaScript-API
1093
+ // vocabulary: a bare `list.source` literal (`On track,Watch,Off track`),
1094
+ // PascalCase operators (`Between`), PascalCase types (`WholeNumber`) and
1095
+ // `=`-prefixed formulas. Excel opens a file carrying any of those in repair
1096
+ // mode and drops the rule, so the exporter normalizes every field before
1097
+ // replaying it, and a type / operator that is not an OOXML token even after
1098
+ // normalization (`undefined` below) is never replayed — the rule falls
1099
+ // through to the condition synthesizer. Twin of `normalize_source_*` in
1100
+ // `rust/crates/rnc-export/src/xlsx/datavalidation.rs` — keep the two in sync
1101
+ // so both engines emit identical XML.
1102
+ const normalizeXlsxSourceType = (type) => {
1103
+ switch (type.trim().toLowerCase()) {
1104
+ case "list":
1105
+ return "list";
1106
+ case "whole":
1107
+ case "wholenumber":
1108
+ return "whole";
1109
+ case "decimal":
1110
+ return "decimal";
1111
+ case "date":
1112
+ return "date";
1113
+ case "time":
1114
+ return "time";
1115
+ case "textlength":
1116
+ return "textLength";
1117
+ case "custom":
1118
+ return "custom";
1119
+ default:
1120
+ return undefined;
1121
+ }
1122
+ };
1123
+ const normalizeXlsxSourceOperator = (operator) => {
1124
+ switch (operator.trim().toLowerCase()) {
1125
+ case "between":
1126
+ return "between";
1127
+ case "notbetween":
1128
+ return "notBetween";
1129
+ case "equal":
1130
+ case "equalto":
1131
+ return "equal";
1132
+ case "notequal":
1133
+ case "notequalto":
1134
+ return "notEqual";
1135
+ case "greaterthan":
1136
+ return "greaterThan";
1137
+ case "lessthan":
1138
+ return "lessThan";
1139
+ case "greaterthanorequal":
1140
+ case "greaterthanorequalto":
1141
+ return "greaterThanOrEqual";
1142
+ case "lessthanorequal":
1143
+ case "lessthanorequalto":
1144
+ return "lessThanOrEqual";
1145
+ default:
1146
+ return undefined;
1147
+ }
1148
+ };
1149
+ // OOXML `<formula1>` text never carries the leading `=` a formula-style
1150
+ // source (`=Sheet1!$A$1:$A$5`) has.
1151
+ const stripLeadingEquals = (formula) => {
1152
+ const trimmed = formula.trim();
1153
+ return trimmed.startsWith("=") ? trimmed.slice(1) : trimmed;
1154
+ };
1155
+ // One identifier token — `[A-Za-z_\\][A-Za-z0-9_.]*` — which is what a
1156
+ // defined name (`StatusList`, `My_List.v2`) or a single A1 cell (`A1`) looks
1157
+ // like; a word list with commas or spaces is not one.
1158
+ const XLSX_IDENTIFIER_TOKEN = /^[A-Za-z_\\][A-Za-z0-9_.]*$/;
1159
+ // A single A1 cell token: 1–3 letters followed by digits.
1160
+ const XLSX_A1_CELL_TOKEN = /^[A-Za-z]{1,3}[0-9]+$/;
1161
+ // A function call — `OFFSET(…)`, `INDIRECT(…)` — which Excel writes without
1162
+ // a leading `=`.
1163
+ const XLSX_FUNCTION_CALL = /^[A-Za-z_\\][A-Za-z0-9_.]*\(.*\)$/;
1164
+ // `true` when a list source reads as a reference / formula rather than as
1165
+ // list text: it carries a reference marker (`!` sheet qualifier, `:` range,
1166
+ // `$` absolute anchor, `[` structured / external ref), is a single A1 cell
1167
+ // or defined-name token, or is a function call. Anything else — comma lists,
1168
+ // text with spaces, digit-only entries — is a bare literal that Excel only
1169
+ // accepts quoted.
1170
+ const looksLikeXlsxListReference = (text) => text.length > 0 &&
1171
+ (/[!:$[]/.test(text) ||
1172
+ XLSX_A1_CELL_TOKEN.test(text) ||
1173
+ XLSX_IDENTIFIER_TOKEN.test(text) ||
1174
+ XLSX_FUNCTION_CALL.test(text));
1175
+ // A `list` source is valid OOXML only as a quoted string literal (`"a,b"`)
1176
+ // or a reference (`$A$1:$A$5`, `Sheet1!$A$1:$A$5`, `StatusList`, `OFFSET(…)`).
1177
+ // Excel's API hands back a literal list as bare comma-separated text and a
1178
+ // reference with a leading `=`, so: quoted → verbatim, `=`-prefixed → the `=`
1179
+ // is stripped and the rest replayed verbatim, reference-like → verbatim,
1180
+ // anything else → a literal that must be quoted (with `"` doubled inside, as
1181
+ // the ONE_OF_LIST branch does).
1182
+ const normalizeXlsxSourceListFormula1 = (formula1) => {
1183
+ const trimmed = formula1.trim();
1184
+ if (trimmed.startsWith('"'))
1185
+ return trimmed;
1186
+ if (trimmed.startsWith("="))
1187
+ return trimmed.slice(1).trim();
1188
+ if (looksLikeXlsxListReference(trimmed))
1189
+ return trimmed;
1190
+ return `"${escapeFormulaText(trimmed)}"`;
1191
+ };
1089
1192
  const convertDataValidationRule = (rule) => {
1090
1193
  if (!rule)
1091
1194
  return undefined;
@@ -1103,20 +1206,58 @@ const convertDataValidationRule = (rule) => {
1103
1206
  prompt: inputMessage?.message ?? undefined,
1104
1207
  };
1105
1208
  // Round-trip path: when the rule was imported from xlsx, replay the
1106
- // verbatim source representation. Preserves type/operator/formulas
1107
- // exactly, including `whole`/`textLength`/`time` types and user-written
1108
- // custom formulas without `INDIRECT("RC",FALSE)` rewriting.
1209
+ // source representation after normalizing it to OOXML vocabulary (see
1210
+ // `normalizeXlsxSource*`). Preserves type/operator/formulas, including
1211
+ // `whole`/`textLength`/`time` types and user-written custom formulas
1212
+ // without `INDIRECT("RC",FALSE)` rewriting. A type / operator that is not
1213
+ // an OOXML token even after normalization (Excel's `Inconsistent`,
1214
+ // `MixedCriteria`) is never written: the rule falls through to the
1215
+ // condition synthesizer.
1109
1216
  const src = rule.xlsxSource;
1217
+ let rejectedListSource;
1110
1218
  if (src && src.type && typeof src.formula1 === "string" && src.formula1) {
1219
+ const type = normalizeXlsxSourceType(src.type);
1220
+ const operator = src.operator
1221
+ ? normalizeXlsxSourceOperator(src.operator)
1222
+ : undefined;
1223
+ const operatorValid = !src.operator || operator !== undefined;
1224
+ if (type !== undefined && operatorValid) {
1225
+ const formula1 = type === "list"
1226
+ ? normalizeXlsxSourceListFormula1(src.formula1)
1227
+ : stripLeadingEquals(src.formula1);
1228
+ return {
1229
+ type,
1230
+ operator,
1231
+ formula1,
1232
+ formula2: src.formula2 ? stripLeadingEquals(src.formula2) : undefined,
1233
+ showDropDown: type === "list" && rule.displayStyle === "plain" ? true : undefined,
1234
+ ...baseConfig,
1235
+ };
1236
+ }
1237
+ if (type === "list") {
1238
+ rejectedListSource = normalizeXlsxSourceListFormula1(src.formula1);
1239
+ }
1240
+ }
1241
+ const synthesized = convertDataValidationCondition(rule, baseConfig);
1242
+ if (synthesized)
1243
+ return synthesized;
1244
+ // A rejected `list` source with no usable condition is still kept — its
1245
+ // normalized source text (quoted literal or reference) is always valid
1246
+ // OOXML. Anything else rejected has nothing valid to write and is skipped.
1247
+ if (rejectedListSource !== undefined) {
1111
1248
  return {
1112
- type: src.type,
1113
- operator: src.operator,
1114
- formula1: src.formula1,
1115
- formula2: src.formula2,
1116
- showDropDown: src.type === "list" && rule.displayStyle === "plain" ? true : undefined,
1249
+ type: "list",
1250
+ formula1: rejectedListSource,
1251
+ showDropDown: rule.displayStyle === "plain" ? true : undefined,
1117
1252
  ...baseConfig,
1118
1253
  };
1119
1254
  }
1255
+ return undefined;
1256
+ };
1257
+ // The condition-based synthesizer: the Excel config for a rule that has no
1258
+ // replayable `xlsxSource` (UI-authored, edited after import, or rejected by
1259
+ // the round-trip guard above).
1260
+ const convertDataValidationCondition = (rule, baseConfig) => {
1120
1261
  if (!rule.condition?.type) {
1121
1262
  // Prompt-only DV (no validation rule, just an input-message tooltip
1122
1263
  // and/or an error alert). Emit it with no type/formula so Excel still
@@ -1129,7 +1270,6 @@ const convertDataValidationRule = (rule) => {
1129
1270
  const { condition } = rule;
1130
1271
  const values = condition.values ?? [];
1131
1272
  const currentCell = 'INDIRECT("RC",FALSE)';
1132
- const escapeFormulaText = (value) => value.replace(/"/g, '""');
1133
1273
  const ensureFormula = (value) => value.startsWith("=") ? value : `=${value}`;
1134
1274
  const ifErrorWrap = (formula) => `IFERROR(${formula},TRUE)`;
1135
1275
  if (condition.type === "ONE_OF_LIST") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rowsncolumns/toolkit",
3
- "version": "24.5.0",
3
+ "version": "24.5.3",
4
4
  "author": "Vinay M",
5
5
  "license": "SEE LICENSE IN LICENSE.md",
6
6
  "main": "./dist/cjs/index.js",
@@ -45,7 +45,7 @@
45
45
  "url": "https://github.com/rowsncolumns/spreadsheet/issues"
46
46
  },
47
47
  "devDependencies": {
48
- "@rowsncolumns/utils": ">=9.4.0",
48
+ "@rowsncolumns/utils": ">=9.5.1",
49
49
  "@types/lodash-es": "^4.17.12"
50
50
  },
51
51
  "peerDependencies": {
@@ -55,9 +55,9 @@
55
55
  "dependencies": {
56
56
  "@noble/ciphers": "^0.5.3",
57
57
  "@noble/hashes": "^1.4.0",
58
- "@rowsncolumns/calculator": ">=7.2.33",
59
- "@rowsncolumns/common-types": ">=5.2.34",
60
- "@rowsncolumns/date-parser": ">=13.0.1",
58
+ "@rowsncolumns/calculator": ">=7.3.1",
59
+ "@rowsncolumns/common-types": ">=5.3.1",
60
+ "@rowsncolumns/date-parser": ">=13.0.2",
61
61
  "@zip.js/zip.js": "^2.8.17",
62
62
  "fast-xml-parser": "^5.8.0",
63
63
  "fflate": "^0.8.2",