@rowsncolumns/utils 9.5.0 → 9.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.
- package/dist/cjs/clipboard-html-sanitize.d.ts +15 -0
- package/dist/cjs/clipboard-html-sanitize.d.ts.map +1 -0
- package/dist/cjs/clipboard-html-sanitize.js +237 -0
- package/dist/cjs/condition-operand.d.ts +54 -0
- package/dist/cjs/condition-operand.d.ts.map +1 -0
- package/dist/cjs/condition-operand.js +223 -0
- package/dist/cjs/conditional-format.d.ts.map +1 -1
- package/dist/cjs/conditional-format.js +5 -59
- package/dist/cjs/data-validation.d.ts +9 -0
- package/dist/cjs/data-validation.d.ts.map +1 -1
- package/dist/cjs/data-validation.js +29 -60
- package/dist/cjs/index.d.ts +4 -0
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js +4 -0
- package/dist/cjs/rich-text-html.d.ts +9 -0
- package/dist/cjs/rich-text-html.d.ts.map +1 -1
- package/dist/cjs/rich-text-html.js +52 -23
- package/dist/cjs/sheet-names.d.ts +60 -0
- package/dist/cjs/sheet-names.d.ts.map +1 -0
- package/dist/cjs/sheet-names.js +157 -0
- package/dist/cjs/sheet-protection.d.ts +9 -0
- package/dist/cjs/sheet-protection.d.ts.map +1 -0
- package/dist/cjs/sheet-protection.js +11 -0
- package/dist/cjs/string.d.ts +31 -0
- package/dist/cjs/string.d.ts.map +1 -1
- package/dist/cjs/string.js +73 -3
- package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
- package/dist/esm/clipboard-html-sanitize.d.ts +15 -0
- package/dist/esm/clipboard-html-sanitize.d.ts.map +1 -0
- package/dist/esm/clipboard-html-sanitize.js +192 -0
- package/dist/esm/condition-operand.d.ts +54 -0
- package/dist/esm/condition-operand.d.ts.map +1 -0
- package/dist/esm/condition-operand.js +203 -0
- package/dist/esm/conditional-format.d.ts.map +1 -1
- package/dist/esm/conditional-format.js +5 -57
- package/dist/esm/data-validation.d.ts +9 -0
- package/dist/esm/data-validation.d.ts.map +1 -1
- package/dist/esm/data-validation.js +27 -57
- package/dist/esm/index.d.ts +4 -0
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +4 -0
- package/dist/esm/rich-text-html.d.ts +9 -0
- package/dist/esm/rich-text-html.d.ts.map +1 -1
- package/dist/esm/rich-text-html.js +20 -4
- package/dist/esm/sheet-names.d.ts +60 -0
- package/dist/esm/sheet-names.d.ts.map +1 -0
- package/dist/esm/sheet-names.js +109 -0
- package/dist/esm/sheet-protection.d.ts +9 -0
- package/dist/esm/sheet-protection.d.ts.map +1 -0
- package/dist/esm/sheet-protection.js +8 -0
- package/dist/esm/string.d.ts +31 -0
- package/dist/esm/string.d.ts.map +1 -1
- package/dist/esm/string.js +62 -2
- package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PROTECTED_SHEET_EDIT_MESSAGE = exports.SHEET_PROTECTION_ERROR_TITLE = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Excel's refusal when an edit touches a locked cell of a protected sheet — the dialog every
|
|
6
|
+
* mutation path shows (typing, Delete / Backspace, fill, paste) on both engines
|
|
7
|
+
* (rowsncolumns/spreadsheet#939 U-35). The rnc store surfaces the same text through its
|
|
8
|
+
* `rejections` channel under the `protected-sheet` code.
|
|
9
|
+
*/
|
|
10
|
+
exports.SHEET_PROTECTION_ERROR_TITLE = "This sheet is protected";
|
|
11
|
+
exports.PROTECTED_SHEET_EDIT_MESSAGE = "The cell or chart you're trying to change is on a protected sheet. To make a change, unprotect the sheet. You might be requested to enter a password.";
|
package/dist/cjs/string.d.ts
CHANGED
|
@@ -38,6 +38,20 @@ export declare const cellToAddress: (cell: CellInterface | null, isAbsoluteColum
|
|
|
38
38
|
* @returns
|
|
39
39
|
*/
|
|
40
40
|
export declare const cellRangeToAddress: (sheetName: string | undefined, range: GridRange) => string;
|
|
41
|
+
/**
|
|
42
|
+
* Must `name` be single-quoted as a sheet qualifier (`'My Data'!A1`)? Excel writes it bare only when
|
|
43
|
+
* it is a plain identifier — ASCII letters and digits, starting with a letter — and cannot be read
|
|
44
|
+
* as something else: a cell reference (`A1`, `XFD1`, `R1C1`), or a boolean (`TRUE`). A leading digit
|
|
45
|
+
* (`2024`, `1Sheet`) always quotes. Deliberately a superset of Excel's minimum (a `_`, `.` or
|
|
46
|
+
* non-ASCII letter also quotes): over-quoting is always valid, under-quoting makes `=2024!A1` a
|
|
47
|
+
* formula Excel rejects (rowsncolumns/spreadsheet#939 K-10).
|
|
48
|
+
*/
|
|
49
|
+
export declare const sheetNameNeedsQuoting: (name: string) => boolean;
|
|
50
|
+
/**
|
|
51
|
+
* Quote a sheet name for use as a formula qualifier when Excel requires it (see
|
|
52
|
+
* {@link sheetNameNeedsQuoting}); apostrophes inside are doubled (`'O''Neil'`). Rename, cross-sheet
|
|
53
|
+
* reference generation and formula-bar insertion all quote through here so they agree.
|
|
54
|
+
*/
|
|
41
55
|
export declare const sanitizeSheetName: (name: string | undefined) => string | undefined;
|
|
42
56
|
/**
|
|
43
57
|
* Remove single quotes from sheetname
|
|
@@ -76,6 +90,23 @@ export declare const matchesWildcardPattern: (pattern: string, text: string, anc
|
|
|
76
90
|
*/
|
|
77
91
|
export declare const selectionBoundsFromCells: (start: CellInterface, end?: CellInterface | null) => GridRange;
|
|
78
92
|
export declare const supplant: (s: string, o: Record<string, any>) => string;
|
|
93
|
+
/**
|
|
94
|
+
* Whether a hyperlink target read from untrusted markup (clipboard HTML) may
|
|
95
|
+
* be kept. A URL with an explicit scheme must use one of the safe schemes;
|
|
96
|
+
* scheme-less, protocol-relative, path and fragment references are allowed
|
|
97
|
+
* (they are normalized by {@link normalizeExternalUrl} before being opened).
|
|
98
|
+
* Applies the same tab/newline and C0-control stripping as the browser's URL
|
|
99
|
+
* canonicalization so `java\tscript:` cannot slip through.
|
|
100
|
+
*/
|
|
101
|
+
export declare const isSafeHyperlinkUri: (uri: string | null | undefined) => boolean;
|
|
102
|
+
/**
|
|
103
|
+
* Escape text for interpolation into an HTML text node (`text/html`
|
|
104
|
+
* clipboard fragments): a cell whose value is literally `<b>x</b>` must paste
|
|
105
|
+
* back as that text, not as bold "x".
|
|
106
|
+
*/
|
|
107
|
+
export declare const escapeHtmlText: (value: string) => string;
|
|
108
|
+
/** Escape a value for interpolation into a quoted HTML attribute. */
|
|
109
|
+
export declare const escapeHtmlAttribute: (value: string) => string;
|
|
79
110
|
/**
|
|
80
111
|
* Normalize an external hyperlink target for the browser. A stored URL
|
|
81
112
|
* without a scheme ("paradigm.xyz") would otherwise be resolved relative to
|
package/dist/cjs/string.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"string.d.ts","sourceRoot":"","sources":["../../string.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAC;AAK3E;;;;;;GAMG;AACH,eAAO,MAAM,kBAAkB,GAAI,MAAM,aAAa,KAAG,SAKvD,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,aAAa,GACxB,SAAS,MAAM,EACf,UAAS;IACP,QAAQ,CAAC,EAAE,OAAO,GAAG,KAAK,CAAC;IAC3B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;CACjB,KACL,aAAa,GAAG,IAuDlB,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,wBAAwB,GAAI,SAAS,MAAM,uBAEvD,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,sBAAsB,GAAI,SAAS,MAAM,uBAErD,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,aAAa,GACxB,MAAM,aAAa,GAAG,IAAI,EAC1B,0BAAwB,EACxB,uBAAqB,EACrB,sBAAoB,EACpB,mBAAiB,KAChB,MAAM,GAAG,IAiBX,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,kBAAkB,GAC7B,WAAW,MAAM,GAAG,SAAS,EAC7B,OAAO,SAAS,WAUjB,CAAC;
|
|
1
|
+
{"version":3,"file":"string.d.ts","sourceRoot":"","sources":["../../string.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAC;AAK3E;;;;;;GAMG;AACH,eAAO,MAAM,kBAAkB,GAAI,MAAM,aAAa,KAAG,SAKvD,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,aAAa,GACxB,SAAS,MAAM,EACf,UAAS;IACP,QAAQ,CAAC,EAAE,OAAO,GAAG,KAAK,CAAC;IAC3B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;CACjB,KACL,aAAa,GAAG,IAuDlB,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,wBAAwB,GAAI,SAAS,MAAM,uBAEvD,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,sBAAsB,GAAI,SAAS,MAAM,uBAErD,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,aAAa,GACxB,MAAM,aAAa,GAAG,IAAI,EAC1B,0BAAwB,EACxB,uBAAqB,EACrB,sBAAoB,EACpB,mBAAiB,KAChB,MAAM,GAAG,IAiBX,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,kBAAkB,GAC7B,WAAW,MAAM,GAAG,SAAS,EAC7B,OAAO,SAAS,WAUjB,CAAC;AAOF;;;;;;;GAOG;AACH,eAAO,MAAM,qBAAqB,GAAI,MAAM,MAAM,KAAG,OAMrB,CAAC;AAEjC;;;;GAIG;AACH,eAAO,MAAM,iBAAiB,GAAI,MAAM,MAAM,GAAG,SAAS,uBASzD,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,mBAAmB,GAAI,MAAM,MAAM,GAAG,SAAS,uBAgB3D,CAAC;AAKF;;;;GAIG;AACH,eAAO,MAAM,gBAAgB,GAAI,KAAK,MAAM,WAI3C,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,6BAA6B,GAAI,SAAS,MAAM,KAAG,MAe/D,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,sBAAsB,GACjC,SAAS,MAAM,EACf,MAAM,MAAM,EACZ,QAAQ,QAAQ,GAAG,YAAY,GAAG,UAAU,GAAG,UAAU,KACxD,OAWF,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,wBAAwB,GACnC,OAAO,aAAa,EACpB,MAAK,aAAa,GAAG,IAAY,KAChC,SAQF,CAAC;AAEF,eAAO,MAAM,QAAQ,GAAI,GAAG,MAAM,EAAE,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,WAKzD,CAAC;AAUF;;;;;;;GAOG;AACH,eAAO,MAAM,kBAAkB,GAAI,KAAK,MAAM,GAAG,IAAI,GAAG,SAAS,KAAG,OAgBnE,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,cAAc,GAAI,OAAO,MAAM,KAAG,MAC2B,CAAC;AAE3E,qEAAqE;AACrE,eAAO,MAAM,mBAAmB,GAAI,OAAO,MAAM,KAAG,MAM1B,CAAC;AAE3B;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,oBAAoB,GAAI,KAAK,MAAM,WAoB/C,CAAC"}
|
package/dist/cjs/string.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.normalizeExternalUrl = exports.supplant = exports.selectionBoundsFromCells = exports.matchesWildcardPattern = exports.wildcardPatternToRegExpSource = exports.escapeCharacters = exports.desanitizeSheetName = exports.sanitizeSheetName = exports.cellRangeToAddress = exports.cellToAddress = exports.getRowIndexFromAddress = exports.getColumnNameFromAddress = exports.addressToCell = exports.convertCellToRange = void 0;
|
|
3
|
+
exports.normalizeExternalUrl = exports.escapeHtmlAttribute = exports.escapeHtmlText = exports.isSafeHyperlinkUri = exports.supplant = exports.selectionBoundsFromCells = exports.matchesWildcardPattern = exports.wildcardPatternToRegExpSource = exports.escapeCharacters = exports.desanitizeSheetName = exports.sanitizeSheetName = exports.sheetNameNeedsQuoting = exports.cellRangeToAddress = exports.cellToAddress = exports.getRowIndexFromAddress = exports.getColumnNameFromAddress = exports.addressToCell = exports.convertCellToRange = void 0;
|
|
4
4
|
var tslib_1 = require("tslib");
|
|
5
5
|
var typeguards_1 = require("./typeguards");
|
|
6
6
|
var numbers_1 = require("./numbers");
|
|
@@ -135,13 +135,38 @@ var cellRangeToAddress = function (sheetName, range) {
|
|
|
135
135
|
}));
|
|
136
136
|
};
|
|
137
137
|
exports.cellRangeToAddress = cellRangeToAddress;
|
|
138
|
-
|
|
138
|
+
/** `A1`-style lookalike: 1–3 ASCII letters followed only by digits (`AB12`, `XFD1`). */
|
|
139
|
+
var SHEET_NAME_A1_LOOKALIKE = /^[A-Za-z]{1,3}\d+$/;
|
|
140
|
+
/** `R1C1`-style lookalike: an optional-digit `R` part and/or `C` part, nothing else (`R`, `C22`, `R1C1`). */
|
|
141
|
+
var SHEET_NAME_R1C1_LOOKALIKE = /^(?:[Rr]\d*)?(?:[Cc]\d*)?$/;
|
|
142
|
+
/**
|
|
143
|
+
* Must `name` be single-quoted as a sheet qualifier (`'My Data'!A1`)? Excel writes it bare only when
|
|
144
|
+
* it is a plain identifier — ASCII letters and digits, starting with a letter — and cannot be read
|
|
145
|
+
* as something else: a cell reference (`A1`, `XFD1`, `R1C1`), or a boolean (`TRUE`). A leading digit
|
|
146
|
+
* (`2024`, `1Sheet`) always quotes. Deliberately a superset of Excel's minimum (a `_`, `.` or
|
|
147
|
+
* non-ASCII letter also quotes): over-quoting is always valid, under-quoting makes `=2024!A1` a
|
|
148
|
+
* formula Excel rejects (rowsncolumns/spreadsheet#939 K-10).
|
|
149
|
+
*/
|
|
150
|
+
var sheetNameNeedsQuoting = function (name) {
|
|
151
|
+
return name.length === 0 ||
|
|
152
|
+
/[^A-Za-z0-9]/.test(name) ||
|
|
153
|
+
/^\d/.test(name) ||
|
|
154
|
+
SHEET_NAME_A1_LOOKALIKE.test(name) ||
|
|
155
|
+
SHEET_NAME_R1C1_LOOKALIKE.test(name) ||
|
|
156
|
+
/^(?:true|false)$/i.test(name);
|
|
157
|
+
};
|
|
158
|
+
exports.sheetNameNeedsQuoting = sheetNameNeedsQuoting;
|
|
159
|
+
/**
|
|
160
|
+
* Quote a sheet name for use as a formula qualifier when Excel requires it (see
|
|
161
|
+
* {@link sheetNameNeedsQuoting}); apostrophes inside are doubled (`'O''Neil'`). Rename, cross-sheet
|
|
162
|
+
* reference generation and formula-bar insertion all quote through here so they agree.
|
|
163
|
+
*/
|
|
139
164
|
var sanitizeSheetName = function (name) {
|
|
140
165
|
if (name === void 0)
|
|
141
166
|
return void 0;
|
|
142
167
|
if (typeof name === "number")
|
|
143
168
|
return name;
|
|
144
|
-
if (
|
|
169
|
+
if ((0, exports.sheetNameNeedsQuoting)(name)) {
|
|
145
170
|
// Escape single quotes by doubling them for valid sheet references.
|
|
146
171
|
var escaped = name.replace(/'/g, "''");
|
|
147
172
|
return "'".concat(escaped, "'");
|
|
@@ -263,6 +288,51 @@ var URL_SCHEME = /^([a-zA-Z][a-zA-Z0-9+.-]*):/;
|
|
|
263
288
|
// explicit scheme (javascript:, data:, vbscript:, …) is a script/content
|
|
264
289
|
// injection vector and is rejected.
|
|
265
290
|
var SAFE_URL_SCHEMES = new Set(["http", "https", "mailto", "tel", "ftp"]);
|
|
291
|
+
/**
|
|
292
|
+
* Whether a hyperlink target read from untrusted markup (clipboard HTML) may
|
|
293
|
+
* be kept. A URL with an explicit scheme must use one of the safe schemes;
|
|
294
|
+
* scheme-less, protocol-relative, path and fragment references are allowed
|
|
295
|
+
* (they are normalized by {@link normalizeExternalUrl} before being opened).
|
|
296
|
+
* Applies the same tab/newline and C0-control stripping as the browser's URL
|
|
297
|
+
* canonicalization so `java\tscript:` cannot slip through.
|
|
298
|
+
*/
|
|
299
|
+
var isSafeHyperlinkUri = function (uri) {
|
|
300
|
+
if (uri == null) {
|
|
301
|
+
return false;
|
|
302
|
+
}
|
|
303
|
+
var trimmed = uri
|
|
304
|
+
.replace(/[\t\n\r]/g, "")
|
|
305
|
+
.replace(/^[\u0000-\u0020]+|[\u0000-\u0020]+$/g, "")
|
|
306
|
+
.trim();
|
|
307
|
+
if (!trimmed) {
|
|
308
|
+
return false;
|
|
309
|
+
}
|
|
310
|
+
var schemeMatch = URL_SCHEME.exec(trimmed);
|
|
311
|
+
if (!schemeMatch) {
|
|
312
|
+
return true;
|
|
313
|
+
}
|
|
314
|
+
return SAFE_URL_SCHEMES.has(schemeMatch[1].toLowerCase());
|
|
315
|
+
};
|
|
316
|
+
exports.isSafeHyperlinkUri = isSafeHyperlinkUri;
|
|
317
|
+
/**
|
|
318
|
+
* Escape text for interpolation into an HTML text node (`text/html`
|
|
319
|
+
* clipboard fragments): a cell whose value is literally `<b>x</b>` must paste
|
|
320
|
+
* back as that text, not as bold "x".
|
|
321
|
+
*/
|
|
322
|
+
var escapeHtmlText = function (value) {
|
|
323
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
324
|
+
};
|
|
325
|
+
exports.escapeHtmlText = escapeHtmlText;
|
|
326
|
+
/** Escape a value for interpolation into a quoted HTML attribute. */
|
|
327
|
+
var escapeHtmlAttribute = function (value) {
|
|
328
|
+
return value
|
|
329
|
+
.replace(/&/g, "&")
|
|
330
|
+
.replace(/"/g, """)
|
|
331
|
+
.replace(/'/g, "'")
|
|
332
|
+
.replace(/</g, "<")
|
|
333
|
+
.replace(/>/g, ">");
|
|
334
|
+
};
|
|
335
|
+
exports.escapeHtmlAttribute = escapeHtmlAttribute;
|
|
266
336
|
/**
|
|
267
337
|
* Normalize an external hyperlink target for the browser. A stored URL
|
|
268
338
|
* without a scheme ("paradigm.xyz") would otherwise be resolved relative to
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"root":["../../array.ts","../../cell-data-helpers.ts","../../color.ts","../../colors.ts","../../conditional-format.ts","../../content-hash.ts","../../data-validation.ts","../../date.ts","../../dom.ts","../../excel-number-format.ts","../../formatter.ts","../../formula-locale.ts","../../formula-refs.ts","../../formula.ts","../../index.ts","../../keyboard.ts","../../locale.ts","../../log.ts","../../memoize.ts","../../numbers.ts","../../rich-text-html.ts","../../rich-text-links.ts","../../selection.ts","../../shared-string-canonical.ts","../../sheet-span.ts","../../sort.ts","../../sparkline-groups.ts","../../string.ts","../../table-names.ts","../../table-styles.ts","../../table-totals.ts","../../typeguards.ts","../../uuid.ts","../../vitest.config.ts","../../formula-locale/formula-locale-data.generated.ts"],"version":"5.8.3"}
|
|
1
|
+
{"root":["../../array.ts","../../cell-data-helpers.ts","../../clipboard-html-sanitize.ts","../../color.ts","../../colors.ts","../../condition-operand.ts","../../conditional-format.ts","../../content-hash.ts","../../data-validation.ts","../../date.ts","../../dom.ts","../../excel-number-format.ts","../../formatter.ts","../../formula-locale.ts","../../formula-refs.ts","../../formula.ts","../../index.ts","../../keyboard.ts","../../locale.ts","../../log.ts","../../memoize.ts","../../numbers.ts","../../rich-text-html.ts","../../rich-text-links.ts","../../selection.ts","../../shared-string-canonical.ts","../../sheet-names.ts","../../sheet-protection.ts","../../sheet-span.ts","../../sort.ts","../../sparkline-groups.ts","../../string.ts","../../table-names.ts","../../table-styles.ts","../../table-totals.ts","../../typeguards.ts","../../uuid.ts","../../vitest.config.ts","../../formula-locale/formula-locale-data.generated.ts"],"version":"5.8.3"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `<img src>` may point at http(s) or an inline `data:image/*` (browsers copy
|
|
3
|
+
* inline images that way). Anything else — `javascript:`, `data:text/html`,
|
|
4
|
+
* `blob:` from another origin — is dropped.
|
|
5
|
+
*/
|
|
6
|
+
export declare const isSafeImageSrc: (value: string | null | undefined) => boolean;
|
|
7
|
+
export declare const sanitizeInlineStyle: (value: string) => string;
|
|
8
|
+
export declare const sanitizeStylesheetText: (value: string) => string;
|
|
9
|
+
/**
|
|
10
|
+
* Strip executable and network-reaching content from an inert (DOMParser)
|
|
11
|
+
* document in place. Safe to call on a document that was never attached; the
|
|
12
|
+
* caller imports the result into a live sandbox afterwards.
|
|
13
|
+
*/
|
|
14
|
+
export declare const sanitizeClipboardDocument: (doc: Document) => Document;
|
|
15
|
+
//# sourceMappingURL=clipboard-html-sanitize.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"clipboard-html-sanitize.d.ts","sourceRoot":"","sources":["../../clipboard-html-sanitize.ts"],"names":[],"mappings":"AA6FA;;;;GAIG;AACH,eAAO,MAAM,cAAc,GAAI,OAAO,MAAM,GAAG,IAAI,GAAG,SAAS,KAAG,OAWjE,CAAC;AAEF,eAAO,MAAM,mBAAmB,GAAI,OAAO,MAAM,KAAG,MAanD,CAAC;AAEF,eAAO,MAAM,sBAAsB,GAAI,OAAO,MAAM,KAAG,MACT,CAAC;AAmC/C;;;;GAIG;AACH,eAAO,MAAM,yBAAyB,GAAI,KAAK,QAAQ,KAAG,QAyCzD,CAAC"}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { isSafeHyperlinkUri } from "./string";
|
|
2
|
+
/**
|
|
3
|
+
* Sanitizer for clipboard `text/html` before any of it touches the live DOM.
|
|
4
|
+
*
|
|
5
|
+
* Clipboard HTML is attacker-controlled: a page the user copied from (or a
|
|
6
|
+
* synthetic `paste` event) can carry `<img onerror>`, `<svg onload>`,
|
|
7
|
+
* `<script>`, `<iframe srcdoc>` or `javascript:` links. The paste converters
|
|
8
|
+
* only ever need the document's structure (tables, rows, cells, inline
|
|
9
|
+
* formatting tags, `data-*` value hints), its inline `style` attributes and
|
|
10
|
+
* the payload's own `<style>` rules (Excel's `.fontN` / `.xlNN` classes), so
|
|
11
|
+
* everything else is dropped from the INERT `DOMParser` document before the
|
|
12
|
+
* sanitized nodes are imported into the mounted sandbox.
|
|
13
|
+
*
|
|
14
|
+
* This runs unconditionally; a consumer's `sanitizeHtml` prop runs on the raw
|
|
15
|
+
* string before this and can only tighten the result.
|
|
16
|
+
*/
|
|
17
|
+
/** Elements removed with their subtree. */
|
|
18
|
+
const FORBIDDEN_ELEMENTS = new Set([
|
|
19
|
+
"script",
|
|
20
|
+
"noscript",
|
|
21
|
+
"template",
|
|
22
|
+
"iframe",
|
|
23
|
+
"frame",
|
|
24
|
+
"frameset",
|
|
25
|
+
"object",
|
|
26
|
+
"embed",
|
|
27
|
+
"applet",
|
|
28
|
+
"link",
|
|
29
|
+
"base",
|
|
30
|
+
"svg",
|
|
31
|
+
"math",
|
|
32
|
+
"input",
|
|
33
|
+
"button",
|
|
34
|
+
"select",
|
|
35
|
+
"textarea",
|
|
36
|
+
"audio",
|
|
37
|
+
"video",
|
|
38
|
+
"source",
|
|
39
|
+
"track",
|
|
40
|
+
"canvas",
|
|
41
|
+
"portal",
|
|
42
|
+
"dialog",
|
|
43
|
+
]);
|
|
44
|
+
/** Elements removed but whose children are kept in place. */
|
|
45
|
+
const UNWRAPPED_ELEMENTS = new Set(["form", "fieldset", "label", "output"]);
|
|
46
|
+
/** URL-bearing attributes that never carry spreadsheet meaning. */
|
|
47
|
+
const FORBIDDEN_ATTRIBUTES = new Set([
|
|
48
|
+
"srcdoc",
|
|
49
|
+
"srcset",
|
|
50
|
+
"formaction",
|
|
51
|
+
"action",
|
|
52
|
+
"background",
|
|
53
|
+
"poster",
|
|
54
|
+
"ping",
|
|
55
|
+
"dynsrc",
|
|
56
|
+
"lowsrc",
|
|
57
|
+
"cite",
|
|
58
|
+
"longdesc",
|
|
59
|
+
"manifest",
|
|
60
|
+
"codebase",
|
|
61
|
+
"classid",
|
|
62
|
+
"data",
|
|
63
|
+
"archive",
|
|
64
|
+
"profile",
|
|
65
|
+
"usemap",
|
|
66
|
+
"xlink:href",
|
|
67
|
+
"xml:base",
|
|
68
|
+
]);
|
|
69
|
+
/** Attributes whose value must be a safe URL; otherwise the attribute is dropped. */
|
|
70
|
+
const URL_ATTRIBUTES = new Set(["href", "src"]);
|
|
71
|
+
/**
|
|
72
|
+
* CSS constructs that can reach the network or (in legacy engines) run code.
|
|
73
|
+
* Any declaration containing one of them is dropped; the rest of the `style`
|
|
74
|
+
* survives so legitimate `font-weight` / `color` / `mso-number-format` hints
|
|
75
|
+
* are kept.
|
|
76
|
+
*/
|
|
77
|
+
const DANGEROUS_CSS = /(expression\s*\(|url\s*\(|@import|behavior\s*:|-moz-binding|javascript:|vbscript:|data:)/i;
|
|
78
|
+
/**
|
|
79
|
+
* Stylesheet-level constructs that can reach the network. `<style>` text is
|
|
80
|
+
* needed for Excel's class rules but never for anything that fetches.
|
|
81
|
+
*/
|
|
82
|
+
const DANGEROUS_STYLESHEET_RULE = /@import[^;]*;?|url\s*\([^)]*\)|expression\s*\([^)]*\)|behavior\s*:[^;}]*|-moz-binding\s*:[^;}]*/gi;
|
|
83
|
+
const isEventHandlerAttribute = (name) => name.length > 2 && name.charCodeAt(0) === 111 /* o */ && name.charCodeAt(1) === 110 /* n */;
|
|
84
|
+
/**
|
|
85
|
+
* `<img src>` may point at http(s) or an inline `data:image/*` (browsers copy
|
|
86
|
+
* inline images that way). Anything else — `javascript:`, `data:text/html`,
|
|
87
|
+
* `blob:` from another origin — is dropped.
|
|
88
|
+
*/
|
|
89
|
+
export const isSafeImageSrc = (value) => {
|
|
90
|
+
if (!value) {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
const trimmed = value.replace(/[\t\n\r]/g, "").trim();
|
|
94
|
+
if (/^data:image\/(png|gif|jpe?g|webp|bmp|svg\+xml|x-icon|tiff);/i.test(trimmed)) {
|
|
95
|
+
// Inline SVG can carry scripts when navigated to, but as an `<img>` source
|
|
96
|
+
// it is rendered in an isolated, script-less document; allow it.
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
return /^https?:\/\//i.test(trimmed) || /^\/\//.test(trimmed);
|
|
100
|
+
};
|
|
101
|
+
export const sanitizeInlineStyle = (value) => {
|
|
102
|
+
// Untouched styles are kept byte-for-byte: Excel's `mso-number-format`
|
|
103
|
+
// patterns carry escaped `\;` and significant spaces that a split/rejoin
|
|
104
|
+
// would mangle.
|
|
105
|
+
if (!DANGEROUS_CSS.test(value)) {
|
|
106
|
+
return value;
|
|
107
|
+
}
|
|
108
|
+
return value
|
|
109
|
+
.split(";")
|
|
110
|
+
.filter((declaration) => declaration.trim() && !DANGEROUS_CSS.test(declaration))
|
|
111
|
+
.join(";");
|
|
112
|
+
};
|
|
113
|
+
export const sanitizeStylesheetText = (value) => value.replace(DANGEROUS_STYLESHEET_RULE, "");
|
|
114
|
+
const sanitizeAttributes = (element) => {
|
|
115
|
+
const tag = element.tagName.toLowerCase();
|
|
116
|
+
// Copy first: removing while iterating a live NamedNodeMap skips entries.
|
|
117
|
+
const attributes = Array.from(element.attributes);
|
|
118
|
+
for (const attribute of attributes) {
|
|
119
|
+
const name = attribute.name.toLowerCase();
|
|
120
|
+
if (isEventHandlerAttribute(name) || FORBIDDEN_ATTRIBUTES.has(name)) {
|
|
121
|
+
element.removeAttribute(attribute.name);
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
if (name === "style") {
|
|
125
|
+
const safe = sanitizeInlineStyle(attribute.value);
|
|
126
|
+
if (safe !== attribute.value) {
|
|
127
|
+
if (safe) {
|
|
128
|
+
element.setAttribute(attribute.name, safe);
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
element.removeAttribute(attribute.name);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
if (URL_ATTRIBUTES.has(name)) {
|
|
137
|
+
const safe = name === "src"
|
|
138
|
+
? tag === "img" && isSafeImageSrc(attribute.value)
|
|
139
|
+
: isSafeHyperlinkUri(attribute.value);
|
|
140
|
+
if (!safe) {
|
|
141
|
+
element.removeAttribute(attribute.name);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
/**
|
|
147
|
+
* Strip executable and network-reaching content from an inert (DOMParser)
|
|
148
|
+
* document in place. Safe to call on a document that was never attached; the
|
|
149
|
+
* caller imports the result into a live sandbox afterwards.
|
|
150
|
+
*/
|
|
151
|
+
export const sanitizeClipboardDocument = (doc) => {
|
|
152
|
+
// A static snapshot: removals below detach subtrees, and detached
|
|
153
|
+
// descendants are skipped via `doc.contains`.
|
|
154
|
+
const elements = Array.from(doc.querySelectorAll("*"));
|
|
155
|
+
for (const element of elements) {
|
|
156
|
+
if (!doc.contains(element)) {
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
const tag = element.tagName.toLowerCase();
|
|
160
|
+
if (FORBIDDEN_ELEMENTS.has(tag)) {
|
|
161
|
+
element.remove();
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
if (UNWRAPPED_ELEMENTS.has(tag)) {
|
|
165
|
+
const parent = element.parentNode;
|
|
166
|
+
if (parent) {
|
|
167
|
+
while (element.firstChild) {
|
|
168
|
+
parent.insertBefore(element.firstChild, element);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
element.remove();
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
if (tag === "style") {
|
|
175
|
+
const text = element.textContent ?? "";
|
|
176
|
+
const safe = sanitizeStylesheetText(text);
|
|
177
|
+
if (safe !== text) {
|
|
178
|
+
element.textContent = safe;
|
|
179
|
+
}
|
|
180
|
+
// A stylesheet never needs attributes beyond `type`/`media`.
|
|
181
|
+
for (const attribute of Array.from(element.attributes)) {
|
|
182
|
+
const name = attribute.name.toLowerCase();
|
|
183
|
+
if (name !== "type" && name !== "media") {
|
|
184
|
+
element.removeAttribute(attribute.name);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
sanitizeAttributes(element);
|
|
190
|
+
}
|
|
191
|
+
return doc;
|
|
192
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { ConditionType } from "@rowsncolumns/common-types";
|
|
2
|
+
/**
|
|
3
|
+
* Whether a condition type compares TEXT — its operand is always a string
|
|
4
|
+
* literal in Excel, even when it looks like a number or a boolean.
|
|
5
|
+
*/
|
|
6
|
+
export declare const isTextConditionType: (type: ConditionType | string | undefined | null) => boolean;
|
|
7
|
+
export declare const escapeConditionFormulaText: (value: string) => string;
|
|
8
|
+
/**
|
|
9
|
+
* The formula fragment for an operand that is a typed LITERAL — a number
|
|
10
|
+
* (`5`, `1e3`, locale `6,5`), a percentage (`50%` → `0.5`), a boolean, or a
|
|
11
|
+
* date / time in the document locale (`2024-01-05`, `1/5/2024`, `10:30` → the
|
|
12
|
+
* serial). `undefined` for anything else (text, references, formulas).
|
|
13
|
+
*/
|
|
14
|
+
export declare const conditionOperandLiteral: (raw: string | undefined | null, locale?: string) => string | undefined;
|
|
15
|
+
/**
|
|
16
|
+
* Whether an operand (with any leading `=` already removed) is an A1
|
|
17
|
+
* reference — a cell, a range, a whole column/row, with `$` anchors and an
|
|
18
|
+
* optional (quoted) sheet qualifier: `B1`, `$B$1`, `B$1`, `Sheet1!$B$1`,
|
|
19
|
+
* `'My Sheet'!$B$1:$C$9`, `$A:$A`.
|
|
20
|
+
*/
|
|
21
|
+
export declare const isConditionOperandReference: (text: string) => boolean;
|
|
22
|
+
/**
|
|
23
|
+
* Whether an operand (with any leading `=` already removed) is a function
|
|
24
|
+
* call — the form OOXML stores a formula bound in (`TODAY()`,
|
|
25
|
+
* `DATE(2024,1,1)`, `LEN($B$1)`): evaluated as a formula, never quoted.
|
|
26
|
+
*/
|
|
27
|
+
export declare const isConditionOperandFormulaCall: (text: string) => boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Normalize one rule operand into the formula fragment the rule builders
|
|
30
|
+
* splice into `=<cell><op><fragment>` (and the xlsx exporters write inside
|
|
31
|
+
* `<formula>` / `<formula1>` — never with a leading `=`).
|
|
32
|
+
*
|
|
33
|
+
* - `=…` → the formula text (leading `=` stripped).
|
|
34
|
+
* - `TEXT_*` types → ALWAYS a quoted string literal (`"1"`, `"TRUE"`).
|
|
35
|
+
* - A complete `"…"` string literal → as is.
|
|
36
|
+
* - An A1 reference (`$B$1`, `B$1`, `Sheet1!$B$1`, `'My Sheet'!$B$1`) → as is.
|
|
37
|
+
* - A function call (`TODAY()`, `DATE(2024,1,1)`, `LEN(A1)`) → as is.
|
|
38
|
+
* - A typed literal (number, `50%`, boolean, date/time in `locale`) → its
|
|
39
|
+
* numeric form (`0.5`, `45296`, `TRUE`).
|
|
40
|
+
* - Anything else → a quoted string literal (`"abc"`).
|
|
41
|
+
*
|
|
42
|
+
* `""` for an empty operand.
|
|
43
|
+
*/
|
|
44
|
+
export declare const normalizeConditionOperand: (raw: string | undefined | null, type: ConditionType | string | undefined | null, locale?: string) => string;
|
|
45
|
+
/**
|
|
46
|
+
* The OOXML `<formula>` / `<formula1>` text for a number/date rule operand:
|
|
47
|
+
* the leading `=` is stripped (Excel never writes one), typed literals are
|
|
48
|
+
* normalized to what Excel itself stores (`50%` → `0.5`, `2024-01-05` → its
|
|
49
|
+
* serial `45296` rather than the arithmetic `2024-01-05` = 2022), and every
|
|
50
|
+
* other operand — references, defined names, function calls — is written
|
|
51
|
+
* verbatim.
|
|
52
|
+
*/
|
|
53
|
+
export declare const conditionOperandToFormulaText: (raw: string | undefined | null, locale?: string) => string;
|
|
54
|
+
//# sourceMappingURL=condition-operand.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"condition-operand.d.ts","sourceRoot":"","sources":["../../condition-operand.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAyChE;;;GAGG;AACH,eAAO,MAAM,mBAAmB,GAC9B,MAAM,aAAa,GAAG,MAAM,GAAG,SAAS,GAAG,IAAI,KAC9C,OAA0D,CAAC;AAE9D,eAAO,MAAM,0BAA0B,GAAI,OAAO,MAAM,WAC7B,CAAC;AAsC5B;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB,GAClC,KAAK,MAAM,GAAG,SAAS,GAAG,IAAI,EAC9B,SAAS,MAAM,KACd,MAAM,GAAG,SA2CX,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,2BAA2B,GAAI,MAAM,MAAM,KAAG,OACU,CAAC;AAEtE;;;;GAIG;AACH,eAAO,MAAM,6BAA6B,GAAI,MAAM,MAAM,KAAG,OAC5B,CAAC;AAElC;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,yBAAyB,GACpC,KAAK,MAAM,GAAG,SAAS,GAAG,IAAI,EAC9B,MAAM,aAAa,GAAG,MAAM,GAAG,SAAS,GAAG,IAAI,EAC/C,SAAS,MAAM,KACd,MA0BF,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,6BAA6B,GACxC,KAAK,MAAM,GAAG,SAAS,GAAG,IAAI,EAC9B,SAAS,MAAM,KACd,MAQF,CAAC"}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { parseDate, parseTime } from "numfmt";
|
|
2
|
+
import { checkIfStringIsReference } from "./selection";
|
|
3
|
+
/**
|
|
4
|
+
* How a conditional-format / data-validation rule operand (`userEnteredValue`)
|
|
5
|
+
* becomes a formula fragment — the ONE place both rule builders and both xlsx
|
|
6
|
+
* exporters classify an operand, so the runtime and the exported file agree.
|
|
7
|
+
*
|
|
8
|
+
* Excel's rule dialog stores every operand as a formula. Typed text becomes a
|
|
9
|
+
* string literal, a typed number a number, a typed date its serial, `50%` the
|
|
10
|
+
* fraction `0.5`, and `=…` stays a formula. The `TEXT_*` rules ("Text that
|
|
11
|
+
* contains…", "begins with", cellIs equal `"5"`) take a string literal no
|
|
12
|
+
* matter what was typed: *begins with "1"* highlights `1abc` AND `123`, and
|
|
13
|
+
* `cellIs equal "5"` matches the text `5` but not the number 5.
|
|
14
|
+
*/
|
|
15
|
+
const NUMBER_LITERAL_PATTERN = /^[+-]?(?:\d+\.?\d*|\.\d+)(?:e[+-]?\d+)?$/i;
|
|
16
|
+
const BOOLEAN_LITERAL_PATTERN = /^(TRUE|FALSE)$/i;
|
|
17
|
+
const PERCENT_LITERAL_PATTERN = /^([+-]?[\d.,\s ]+)%$/;
|
|
18
|
+
/** A complete formula string literal — `"abc"`, `"say ""hi"""`. */
|
|
19
|
+
const QUOTED_STRING_LITERAL_PATTERN = /^"(?:[^"]|"")*"$/;
|
|
20
|
+
/**
|
|
21
|
+
* A function call — `TODAY()`, `DATE(2024,1,1)`, `LEN($B$1)`, `_xlfn.XLOOKUP(…)` — which is how
|
|
22
|
+
* OOXML stores a formula bound (`<formula1>TODAY()</formula1>`, never with a leading `=`).
|
|
23
|
+
*/
|
|
24
|
+
const FORMULA_CALL_PATTERN = /^[A-Z_\\][\w.]*\([\s\S]*\)$/i;
|
|
25
|
+
/** Whole-column / whole-row spans (`A:A`, `$A:$C`, `1:1`), optionally sheet-qualified. */
|
|
26
|
+
const AXIS_REFERENCE_PATTERN = /^(?:[^!]+!)?(?:\$?[A-Z]{1,3}:\$?[A-Z]{1,3}|\$?\d+:\$?\d+)$/i;
|
|
27
|
+
const TEXT_CONDITION_TYPES = new Set([
|
|
28
|
+
"TEXT_EQ",
|
|
29
|
+
"TEXT_NOT_EQ",
|
|
30
|
+
"TEXT_CONTAINS",
|
|
31
|
+
"TEXT_NOT_CONTAINS",
|
|
32
|
+
"TEXT_STARTS_WITH",
|
|
33
|
+
"TEXT_ENDS_WITH",
|
|
34
|
+
]);
|
|
35
|
+
/**
|
|
36
|
+
* Whether a condition type compares TEXT — its operand is always a string
|
|
37
|
+
* literal in Excel, even when it looks like a number or a boolean.
|
|
38
|
+
*/
|
|
39
|
+
export const isTextConditionType = (type) => Boolean(type && TEXT_CONDITION_TYPES.has(type));
|
|
40
|
+
export const escapeConditionFormulaText = (value) => value.replace(/"/g, '""');
|
|
41
|
+
/**
|
|
42
|
+
* Parse a locale-formatted numeric string (e.g., "6,5" in fr-FR) into a JS
|
|
43
|
+
* number. Uses `Intl.NumberFormat` to detect decimal/group separators.
|
|
44
|
+
* Returns NaN on failure.
|
|
45
|
+
*/
|
|
46
|
+
const parseLocaleNumberLoose = (value, locale) => {
|
|
47
|
+
try {
|
|
48
|
+
const parts = new Intl.NumberFormat(locale).formatToParts(1234.5);
|
|
49
|
+
const decimal = parts.find((part) => part.type === "decimal")?.value ?? ".";
|
|
50
|
+
const group = parts.find((part) => part.type === "group")?.value ?? ",";
|
|
51
|
+
const cleaned = value.split(group).join("").replace(decimal, ".");
|
|
52
|
+
if (!cleaned.trim() || !NUMBER_LITERAL_PATTERN.test(cleaned.trim())) {
|
|
53
|
+
return NaN;
|
|
54
|
+
}
|
|
55
|
+
return Number(cleaned);
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return NaN;
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
/** Shortest decimal text of a number rounded to Excel's 15 significant digits. */
|
|
62
|
+
const formatNumberOperand = (value) => String(Number(value.toPrecision(15)));
|
|
63
|
+
const parseNumberOperand = (text, locale) => {
|
|
64
|
+
if (NUMBER_LITERAL_PATTERN.test(text)) {
|
|
65
|
+
const value = Number(text);
|
|
66
|
+
return Number.isFinite(value) ? value : undefined;
|
|
67
|
+
}
|
|
68
|
+
if (locale) {
|
|
69
|
+
const value = parseLocaleNumberLoose(text, locale);
|
|
70
|
+
return Number.isFinite(value) ? value : undefined;
|
|
71
|
+
}
|
|
72
|
+
return undefined;
|
|
73
|
+
};
|
|
74
|
+
/**
|
|
75
|
+
* The formula fragment for an operand that is a typed LITERAL — a number
|
|
76
|
+
* (`5`, `1e3`, locale `6,5`), a percentage (`50%` → `0.5`), a boolean, or a
|
|
77
|
+
* date / time in the document locale (`2024-01-05`, `1/5/2024`, `10:30` → the
|
|
78
|
+
* serial). `undefined` for anything else (text, references, formulas).
|
|
79
|
+
*/
|
|
80
|
+
export const conditionOperandLiteral = (raw, locale) => {
|
|
81
|
+
if (!raw)
|
|
82
|
+
return undefined;
|
|
83
|
+
const trimmed = raw.trim();
|
|
84
|
+
if (!trimmed)
|
|
85
|
+
return undefined;
|
|
86
|
+
if (BOOLEAN_LITERAL_PATTERN.test(trimmed)) {
|
|
87
|
+
return trimmed.toUpperCase();
|
|
88
|
+
}
|
|
89
|
+
if (NUMBER_LITERAL_PATTERN.test(trimmed)) {
|
|
90
|
+
// Keep the typed digits — `6.50` and `6.5` are the same number and the
|
|
91
|
+
// formula parser reads either.
|
|
92
|
+
return Number.isFinite(Number(trimmed)) ? trimmed : undefined;
|
|
93
|
+
}
|
|
94
|
+
const percent = PERCENT_LITERAL_PATTERN.exec(trimmed);
|
|
95
|
+
if (percent) {
|
|
96
|
+
const value = parseNumberOperand(percent[1].trim(), locale ?? "en-US");
|
|
97
|
+
if (value !== undefined) {
|
|
98
|
+
return formatNumberOperand(value / 100);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
// Dates (incl. date+time) and times, in the document locale's field order
|
|
102
|
+
// (`1/5/2024` is Jan 5 in en-US, May 1 in en-GB; `1.5.2024` is a de-DE
|
|
103
|
+
// date, not the grouped number 152024). numfmt's default locale is
|
|
104
|
+
// month-first, matching the app default.
|
|
105
|
+
try {
|
|
106
|
+
const dateOptions = locale ? { locale } : undefined;
|
|
107
|
+
const date = parseDate(trimmed, dateOptions);
|
|
108
|
+
if (date && typeof date.v === "number" && Number.isFinite(date.v)) {
|
|
109
|
+
return formatNumberOperand(date.v);
|
|
110
|
+
}
|
|
111
|
+
const time = parseTime(trimmed);
|
|
112
|
+
if (time && typeof time.v === "number" && Number.isFinite(time.v)) {
|
|
113
|
+
return formatNumberOperand(time.v);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
// numfmt throws on malformed locale tags — the operand is then plain text.
|
|
118
|
+
}
|
|
119
|
+
if (locale) {
|
|
120
|
+
const value = parseLocaleNumberLoose(trimmed, locale);
|
|
121
|
+
if (Number.isFinite(value)) {
|
|
122
|
+
return formatNumberOperand(value);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return undefined;
|
|
126
|
+
};
|
|
127
|
+
/**
|
|
128
|
+
* Whether an operand (with any leading `=` already removed) is an A1
|
|
129
|
+
* reference — a cell, a range, a whole column/row, with `$` anchors and an
|
|
130
|
+
* optional (quoted) sheet qualifier: `B1`, `$B$1`, `B$1`, `Sheet1!$B$1`,
|
|
131
|
+
* `'My Sheet'!$B$1:$C$9`, `$A:$A`.
|
|
132
|
+
*/
|
|
133
|
+
export const isConditionOperandReference = (text) => checkIfStringIsReference(text) || AXIS_REFERENCE_PATTERN.test(text);
|
|
134
|
+
/**
|
|
135
|
+
* Whether an operand (with any leading `=` already removed) is a function
|
|
136
|
+
* call — the form OOXML stores a formula bound in (`TODAY()`,
|
|
137
|
+
* `DATE(2024,1,1)`, `LEN($B$1)`): evaluated as a formula, never quoted.
|
|
138
|
+
*/
|
|
139
|
+
export const isConditionOperandFormulaCall = (text) => FORMULA_CALL_PATTERN.test(text);
|
|
140
|
+
/**
|
|
141
|
+
* Normalize one rule operand into the formula fragment the rule builders
|
|
142
|
+
* splice into `=<cell><op><fragment>` (and the xlsx exporters write inside
|
|
143
|
+
* `<formula>` / `<formula1>` — never with a leading `=`).
|
|
144
|
+
*
|
|
145
|
+
* - `=…` → the formula text (leading `=` stripped).
|
|
146
|
+
* - `TEXT_*` types → ALWAYS a quoted string literal (`"1"`, `"TRUE"`).
|
|
147
|
+
* - A complete `"…"` string literal → as is.
|
|
148
|
+
* - An A1 reference (`$B$1`, `B$1`, `Sheet1!$B$1`, `'My Sheet'!$B$1`) → as is.
|
|
149
|
+
* - A function call (`TODAY()`, `DATE(2024,1,1)`, `LEN(A1)`) → as is.
|
|
150
|
+
* - A typed literal (number, `50%`, boolean, date/time in `locale`) → its
|
|
151
|
+
* numeric form (`0.5`, `45296`, `TRUE`).
|
|
152
|
+
* - Anything else → a quoted string literal (`"abc"`).
|
|
153
|
+
*
|
|
154
|
+
* `""` for an empty operand.
|
|
155
|
+
*/
|
|
156
|
+
export const normalizeConditionOperand = (raw, type, locale) => {
|
|
157
|
+
if (!raw)
|
|
158
|
+
return "";
|
|
159
|
+
const trimmed = raw.trim();
|
|
160
|
+
if (!trimmed)
|
|
161
|
+
return "";
|
|
162
|
+
if (trimmed.startsWith("=")) {
|
|
163
|
+
return trimmed.slice(1);
|
|
164
|
+
}
|
|
165
|
+
if (isTextConditionType(type)) {
|
|
166
|
+
return `"${escapeConditionFormulaText(trimmed)}"`;
|
|
167
|
+
}
|
|
168
|
+
if (QUOTED_STRING_LITERAL_PATTERN.test(trimmed)) {
|
|
169
|
+
return trimmed;
|
|
170
|
+
}
|
|
171
|
+
if (checkIfStringIsReference(trimmed) || isConditionOperandFormulaCall(trimmed)) {
|
|
172
|
+
return trimmed;
|
|
173
|
+
}
|
|
174
|
+
// Typed literals before whole-row spans: `10:30` is the time 10:30, not
|
|
175
|
+
// rows 10–30 (Excel's dialog reads it the way a cell entry would).
|
|
176
|
+
const literal = conditionOperandLiteral(trimmed, locale);
|
|
177
|
+
if (literal !== undefined) {
|
|
178
|
+
return literal;
|
|
179
|
+
}
|
|
180
|
+
if (isConditionOperandReference(trimmed)) {
|
|
181
|
+
return trimmed;
|
|
182
|
+
}
|
|
183
|
+
return `"${escapeConditionFormulaText(trimmed)}"`;
|
|
184
|
+
};
|
|
185
|
+
/**
|
|
186
|
+
* The OOXML `<formula>` / `<formula1>` text for a number/date rule operand:
|
|
187
|
+
* the leading `=` is stripped (Excel never writes one), typed literals are
|
|
188
|
+
* normalized to what Excel itself stores (`50%` → `0.5`, `2024-01-05` → its
|
|
189
|
+
* serial `45296` rather than the arithmetic `2024-01-05` = 2022), and every
|
|
190
|
+
* other operand — references, defined names, function calls — is written
|
|
191
|
+
* verbatim.
|
|
192
|
+
*/
|
|
193
|
+
export const conditionOperandToFormulaText = (raw, locale) => {
|
|
194
|
+
if (!raw)
|
|
195
|
+
return "";
|
|
196
|
+
const trimmed = raw.trim();
|
|
197
|
+
if (!trimmed)
|
|
198
|
+
return "";
|
|
199
|
+
if (trimmed.startsWith("=")) {
|
|
200
|
+
return trimmed.slice(1);
|
|
201
|
+
}
|
|
202
|
+
return conditionOperandLiteral(trimmed, locale) ?? trimmed;
|
|
203
|
+
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"conditional-format.d.ts","sourceRoot":"","sources":["../../conditional-format.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,gBAAgB,EAEjB,MAAM,4BAA4B,CAAC;
|
|
1
|
+
{"version":3,"file":"conditional-format.d.ts","sourceRoot":"","sources":["../../conditional-format.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,gBAAgB,EAEjB,MAAM,4BAA4B,CAAC;AAQpC;;;;;GAKG;AACH,MAAM,MAAM,yBAAyB,GAAG;IACtC,WAAW,CAAC,EAAE;QAAE,SAAS,CAAC,EAAE,gBAAgB,GAAG,IAAI,CAAA;KAAE,GAAG,IAAI,CAAC;CAC9D,CAAC;AAIF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,eAAO,MAAM,8BAA8B,GACzC,MAAM,yBAAyB,GAAG,SAAS,YAK5C,CAAC;AAEF,eAAO,MAAM,gCAAgC,GAC3C,MAAM,yBAAyB,GAAG,SAAS,EAC3C,aAAa,MAAM,EACnB,SAAS,MAAM,uBA8HhB,CAAC"}
|