@rowsncolumns/toolkit 24.5.0 → 24.5.1
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.
|
@@ -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
|
|
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
|
-
//
|
|
1466
|
-
//
|
|
1467
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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 = (
|
|
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 = (
|
|
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 = (
|
|
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 = (
|
|
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 = (
|
|
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 = (
|
|
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 = (
|
|
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 = (
|
|
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 = (
|
|
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 = (
|
|
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 = (
|
|
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",
|
|
@@ -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
|
-
//
|
|
1107
|
-
//
|
|
1108
|
-
//
|
|
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:
|
|
1113
|
-
|
|
1114
|
-
|
|
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.
|
|
3
|
+
"version": "24.5.1",
|
|
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.
|
|
48
|
+
"@rowsncolumns/utils": ">=9.5.0",
|
|
49
49
|
"@types/lodash-es": "^4.17.12"
|
|
50
50
|
},
|
|
51
51
|
"peerDependencies": {
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
"@noble/ciphers": "^0.5.3",
|
|
57
57
|
"@noble/hashes": "^1.4.0",
|
|
58
58
|
"@rowsncolumns/calculator": ">=7.2.33",
|
|
59
|
-
"@rowsncolumns/common-types": ">=5.
|
|
59
|
+
"@rowsncolumns/common-types": ">=5.3.0",
|
|
60
60
|
"@rowsncolumns/date-parser": ">=13.0.1",
|
|
61
61
|
"@zip.js/zip.js": "^2.8.17",
|
|
62
62
|
"fast-xml-parser": "^5.8.0",
|