@rowsncolumns/utils 9.2.35
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/LICENSE.md +100 -0
- package/dist/cjs/array.d.ts +3 -0
- package/dist/cjs/array.d.ts.map +1 -0
- package/dist/cjs/array.js +19 -0
- package/dist/cjs/cell-data-helpers.d.ts +19 -0
- package/dist/cjs/cell-data-helpers.d.ts.map +1 -0
- package/dist/cjs/cell-data-helpers.js +64 -0
- package/dist/cjs/color.d.ts +20 -0
- package/dist/cjs/color.d.ts.map +1 -0
- package/dist/cjs/color.js +168 -0
- package/dist/cjs/colors.d.ts +2 -0
- package/dist/cjs/colors.d.ts.map +1 -0
- package/dist/cjs/colors.js +78 -0
- package/dist/cjs/conditional-format.d.ts +49 -0
- package/dist/cjs/conditional-format.d.ts.map +1 -0
- package/dist/cjs/conditional-format.js +227 -0
- package/dist/cjs/content-hash.d.ts +16 -0
- package/dist/cjs/content-hash.d.ts.map +1 -0
- package/dist/cjs/content-hash.js +72 -0
- package/dist/cjs/data-validation.d.ts +54 -0
- package/dist/cjs/data-validation.d.ts.map +1 -0
- package/dist/cjs/data-validation.js +249 -0
- package/dist/cjs/date.d.ts +15 -0
- package/dist/cjs/date.d.ts.map +1 -0
- package/dist/cjs/date.js +56 -0
- package/dist/cjs/dom.d.ts +10 -0
- package/dist/cjs/dom.d.ts.map +1 -0
- package/dist/cjs/dom.js +97 -0
- package/dist/cjs/formatter.d.ts +22 -0
- package/dist/cjs/formatter.d.ts.map +1 -0
- package/dist/cjs/formatter.js +264 -0
- package/dist/cjs/formula-refs.d.ts +7 -0
- package/dist/cjs/formula-refs.d.ts.map +1 -0
- package/dist/cjs/formula-refs.js +54 -0
- package/dist/cjs/formula.d.ts +80 -0
- package/dist/cjs/formula.d.ts.map +1 -0
- package/dist/cjs/formula.js +401 -0
- package/dist/cjs/index.d.ts +24 -0
- package/dist/cjs/index.d.ts.map +1 -0
- package/dist/cjs/index.js +26 -0
- package/dist/cjs/locale.d.ts +34 -0
- package/dist/cjs/locale.d.ts.map +1 -0
- package/dist/cjs/locale.js +260 -0
- package/dist/cjs/log.d.ts +4 -0
- package/dist/cjs/log.d.ts.map +1 -0
- package/dist/cjs/log.js +28 -0
- package/dist/cjs/memoize.d.ts +18 -0
- package/dist/cjs/memoize.d.ts.map +1 -0
- package/dist/cjs/memoize.js +128 -0
- package/dist/cjs/numbers.d.ts +19 -0
- package/dist/cjs/numbers.d.ts.map +1 -0
- package/dist/cjs/numbers.js +47 -0
- package/dist/cjs/selection.d.ts +88 -0
- package/dist/cjs/selection.d.ts.map +1 -0
- package/dist/cjs/selection.js +368 -0
- package/dist/cjs/shared-string-canonical.d.ts +18 -0
- package/dist/cjs/shared-string-canonical.d.ts.map +1 -0
- package/dist/cjs/shared-string-canonical.js +74 -0
- package/dist/cjs/sort.d.ts +15 -0
- package/dist/cjs/sort.d.ts.map +1 -0
- package/dist/cjs/sort.js +44 -0
- package/dist/cjs/string.d.ts +74 -0
- package/dist/cjs/string.d.ts.map +1 -0
- package/dist/cjs/string.js +237 -0
- package/dist/cjs/table-styles.d.ts +56 -0
- package/dist/cjs/table-styles.d.ts.map +1 -0
- package/dist/cjs/table-styles.js +362 -0
- package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -0
- package/dist/cjs/typeguards.d.ts +4 -0
- package/dist/cjs/typeguards.d.ts.map +1 -0
- package/dist/cjs/typeguards.js +15 -0
- package/dist/cjs/uuid.d.ts +8 -0
- package/dist/cjs/uuid.d.ts.map +1 -0
- package/dist/cjs/uuid.js +30 -0
- package/dist/cjs/vitest.config.d.ts +3 -0
- package/dist/cjs/vitest.config.d.ts.map +1 -0
- package/dist/cjs/vitest.config.js +9 -0
- package/dist/esm/array.d.ts +3 -0
- package/dist/esm/array.d.ts.map +1 -0
- package/dist/esm/array.js +14 -0
- package/dist/esm/cell-data-helpers.d.ts +19 -0
- package/dist/esm/cell-data-helpers.d.ts.map +1 -0
- package/dist/esm/cell-data-helpers.js +39 -0
- package/dist/esm/color.d.ts +20 -0
- package/dist/esm/color.d.ts.map +1 -0
- package/dist/esm/color.js +152 -0
- package/dist/esm/colors.d.ts +2 -0
- package/dist/esm/colors.d.ts.map +1 -0
- package/dist/esm/colors.js +75 -0
- package/dist/esm/conditional-format.d.ts +49 -0
- package/dist/esm/conditional-format.d.ts.map +1 -0
- package/dist/esm/conditional-format.js +219 -0
- package/dist/esm/content-hash.d.ts +16 -0
- package/dist/esm/content-hash.d.ts.map +1 -0
- package/dist/esm/content-hash.js +68 -0
- package/dist/esm/data-validation.d.ts +54 -0
- package/dist/esm/data-validation.d.ts.map +1 -0
- package/dist/esm/data-validation.js +237 -0
- package/dist/esm/date.d.ts +15 -0
- package/dist/esm/date.d.ts.map +1 -0
- package/dist/esm/date.js +46 -0
- package/dist/esm/dom.d.ts +10 -0
- package/dist/esm/dom.d.ts.map +1 -0
- package/dist/esm/dom.js +90 -0
- package/dist/esm/formatter.d.ts +22 -0
- package/dist/esm/formatter.d.ts.map +1 -0
- package/dist/esm/formatter.js +253 -0
- package/dist/esm/formula-refs.d.ts +7 -0
- package/dist/esm/formula-refs.d.ts.map +1 -0
- package/dist/esm/formula-refs.js +49 -0
- package/dist/esm/formula.d.ts +80 -0
- package/dist/esm/formula.d.ts.map +1 -0
- package/dist/esm/formula.js +298 -0
- package/dist/esm/index.d.ts +24 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +23 -0
- package/dist/esm/locale.d.ts +34 -0
- package/dist/esm/locale.d.ts.map +1 -0
- package/dist/esm/locale.js +223 -0
- package/dist/esm/log.d.ts +4 -0
- package/dist/esm/log.d.ts.map +1 -0
- package/dist/esm/log.js +3 -0
- package/dist/esm/memoize.d.ts +18 -0
- package/dist/esm/memoize.d.ts.map +1 -0
- package/dist/esm/memoize.js +95 -0
- package/dist/esm/numbers.d.ts +19 -0
- package/dist/esm/numbers.d.ts.map +1 -0
- package/dist/esm/numbers.js +40 -0
- package/dist/esm/selection.d.ts +88 -0
- package/dist/esm/selection.d.ts.map +1 -0
- package/dist/esm/selection.js +311 -0
- package/dist/esm/shared-string-canonical.d.ts +18 -0
- package/dist/esm/shared-string-canonical.d.ts.map +1 -0
- package/dist/esm/shared-string-canonical.js +68 -0
- package/dist/esm/sort.d.ts +15 -0
- package/dist/esm/sort.d.ts.map +1 -0
- package/dist/esm/sort.js +39 -0
- package/dist/esm/string.d.ts +74 -0
- package/dist/esm/string.d.ts.map +1 -0
- package/dist/esm/string.js +213 -0
- package/dist/esm/table-styles.d.ts +56 -0
- package/dist/esm/table-styles.d.ts.map +1 -0
- package/dist/esm/table-styles.js +367 -0
- package/dist/esm/tsconfig.esm.tsbuildinfo +1 -0
- package/dist/esm/typeguards.d.ts +4 -0
- package/dist/esm/typeguards.d.ts.map +1 -0
- package/dist/esm/typeguards.js +7 -0
- package/dist/esm/uuid.d.ts +8 -0
- package/dist/esm/uuid.d.ts.map +1 -0
- package/dist/esm/uuid.js +25 -0
- package/dist/esm/vitest.config.d.ts +3 -0
- package/dist/esm/vitest.config.d.ts.map +1 -0
- package/dist/esm/vitest.config.js +7 -0
- package/package.json +49 -0
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { contentHash } from "./content-hash";
|
|
2
|
+
import { isNil } from "./typeguards";
|
|
3
|
+
export const memoize = (fn) => {
|
|
4
|
+
const cache = new Map();
|
|
5
|
+
return (...args) => {
|
|
6
|
+
const key = String(args);
|
|
7
|
+
if (cache.has(key)) {
|
|
8
|
+
return cache.get(key);
|
|
9
|
+
}
|
|
10
|
+
const value = fn.call(undefined, ...args);
|
|
11
|
+
cache.set(key, value);
|
|
12
|
+
return value;
|
|
13
|
+
};
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Mint a stable cellXfs key ("sid") for a CellFormat. The key is
|
|
17
|
+
* `contentHash` (FNV-1a-64 → 8-char base62) over a delimiter-separated
|
|
18
|
+
* canonical serialization of the format's defined properties — the
|
|
19
|
+
* previous 31-bit rolling int hash silently collided two different
|
|
20
|
+
* formats onto one registry key (first-writer wins, so e.g. a red
|
|
21
|
+
* format could hijack a blue one on import).
|
|
22
|
+
*
|
|
23
|
+
* Sids are minted at import/edit time and persisted in the doc's
|
|
24
|
+
* cellXfs map together with the cell references that carry them —
|
|
25
|
+
* readback resolves a stored sid against the stored map, never by
|
|
26
|
+
* re-hashing. Changing this algorithm therefore only affects newly
|
|
27
|
+
* minted keys; existing docs keep resolving their old sids.
|
|
28
|
+
*/
|
|
29
|
+
export const getCellXfsKey = (userObj) => {
|
|
30
|
+
if (userObj === null || userObj === undefined) {
|
|
31
|
+
return String(0);
|
|
32
|
+
}
|
|
33
|
+
const obj = definedProps(userObj);
|
|
34
|
+
let serialized = "";
|
|
35
|
+
const hashString = (str) => {
|
|
36
|
+
// `\u0001` terminates every atom so adjacent strings can't merge
|
|
37
|
+
// into the same serialization (`{a:"bc"}` vs `{ab:"c"}`).
|
|
38
|
+
serialized += str + "\u0001";
|
|
39
|
+
};
|
|
40
|
+
const hashValue = (value) => {
|
|
41
|
+
if (value === null || value === undefined)
|
|
42
|
+
return;
|
|
43
|
+
if (typeof value === "object") {
|
|
44
|
+
// Custom serialization for known object types
|
|
45
|
+
const sortedKeys = Object.keys(value).sort();
|
|
46
|
+
serialized += "\u0002";
|
|
47
|
+
for (const key of sortedKeys) {
|
|
48
|
+
hashString(key);
|
|
49
|
+
hashValue(value[key]);
|
|
50
|
+
}
|
|
51
|
+
serialized += "\u0003";
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
hashString(String(value));
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
// Hash each property in a consistent order. The list MUST cover every
|
|
58
|
+
// field that distinguishes one CellFormat from another — when two
|
|
59
|
+
// different formats hash to the same key, the cellXfs registry
|
|
60
|
+
// dedupes them, so cells lose any field that wasn't in this list.
|
|
61
|
+
const props = [
|
|
62
|
+
"backgroundColor",
|
|
63
|
+
"borders",
|
|
64
|
+
"textFormat",
|
|
65
|
+
"numberFormat",
|
|
66
|
+
"horizontalAlignment",
|
|
67
|
+
"verticalAlignment",
|
|
68
|
+
"wrapStrategy",
|
|
69
|
+
"indent",
|
|
70
|
+
"textRotation",
|
|
71
|
+
"shrinkToFit",
|
|
72
|
+
"relativeIndent",
|
|
73
|
+
"fillPattern",
|
|
74
|
+
"foregroundColor",
|
|
75
|
+
"gradient",
|
|
76
|
+
];
|
|
77
|
+
for (const prop of props) {
|
|
78
|
+
if (obj[prop] !== undefined) {
|
|
79
|
+
hashString(String(prop)); // Include property name in hash
|
|
80
|
+
hashValue(obj[prop]);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return contentHash(serialized);
|
|
84
|
+
};
|
|
85
|
+
export const definedProps = (obj) => {
|
|
86
|
+
if (isNil(obj))
|
|
87
|
+
return obj;
|
|
88
|
+
const result = {};
|
|
89
|
+
for (const key in obj) {
|
|
90
|
+
if (!isNil(obj[key])) {
|
|
91
|
+
result[key] = obj[key];
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return result;
|
|
95
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Convert number to alphabet
|
|
3
|
+
* @param i
|
|
4
|
+
*/
|
|
5
|
+
export declare const number2Alpha: (i: number) => string;
|
|
6
|
+
/**
|
|
7
|
+
* Converts a letter to number
|
|
8
|
+
* A = 1
|
|
9
|
+
* B => 2
|
|
10
|
+
* @param letters
|
|
11
|
+
*/
|
|
12
|
+
export declare const alpha2number: (letters: string) => number;
|
|
13
|
+
/**
|
|
14
|
+
* Get min max of an array
|
|
15
|
+
* @param arr
|
|
16
|
+
*/
|
|
17
|
+
export declare const getMinMaxArray: (arr: number[]) => number[];
|
|
18
|
+
export declare const isNumber: (value: any) => value is number;
|
|
19
|
+
//# sourceMappingURL=numbers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"numbers.d.ts","sourceRoot":"","sources":["../../numbers.ts"],"names":[],"mappings":"AAGA;;;GAGG;AACH,eAAO,MAAM,YAAY,GAAI,GAAG,MAAM,KAAG,MAIxC,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,YAAY,GAAI,SAAS,MAAM,KAAG,MAE9C,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,cAAc,GAAI,KAAK,MAAM,EAAE,aAa3C,CAAC;AAEF,eAAO,MAAM,QAAQ,GAAI,OAAO,GAAG,KAAG,KAAK,IAAI,MAAuB,CAAC"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// Pre-compute alphabet string to avoid repeated string access
|
|
2
|
+
const ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
|
3
|
+
/**
|
|
4
|
+
* Convert number to alphabet
|
|
5
|
+
* @param i
|
|
6
|
+
*/
|
|
7
|
+
export const number2Alpha = (i) => {
|
|
8
|
+
return ((i >= 26 ? number2Alpha(((i / 26) >> 0) - 1) : "") + ALPHABET[i % 26 >> 0]);
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Converts a letter to number
|
|
12
|
+
* A = 1
|
|
13
|
+
* B => 2
|
|
14
|
+
* @param letters
|
|
15
|
+
*/
|
|
16
|
+
export const alpha2number = (letters) => {
|
|
17
|
+
return letters.split("").reduce((r, a) => r * 26 + parseInt(a, 36) - 9, 0);
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Get min max of an array
|
|
21
|
+
* @param arr
|
|
22
|
+
*/
|
|
23
|
+
export const getMinMaxArray = (arr) => {
|
|
24
|
+
if (!arr.length)
|
|
25
|
+
return [0, 0];
|
|
26
|
+
// Loop, not spread — passing >~65k elements as call arguments throws
|
|
27
|
+
// `RangeError: Maximum call stack size exceeded` (selection-sized arrays
|
|
28
|
+
// reach millions).
|
|
29
|
+
let min = arr[0];
|
|
30
|
+
let max = arr[0];
|
|
31
|
+
for (let i = 1; i < arr.length; i++) {
|
|
32
|
+
const v = arr[i];
|
|
33
|
+
if (v < min)
|
|
34
|
+
min = v;
|
|
35
|
+
if (v > max)
|
|
36
|
+
max = v;
|
|
37
|
+
}
|
|
38
|
+
return [min, max];
|
|
39
|
+
};
|
|
40
|
+
export const isNumber = (value) => !isNaN(value);
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import type { CellInterface, GridRange, SheetRange, SelectionArea } from "@rowsncolumns/common-types";
|
|
2
|
+
import { type ParsedToken } from "@rowsncolumns/calculator";
|
|
3
|
+
export type SelectionWithSheetName = SelectionArea & {
|
|
4
|
+
sheetName?: string;
|
|
5
|
+
};
|
|
6
|
+
export type SelectionFromTableResolver = (sheetId: number, activeCell?: CellInterface, tableName?: string, columnNames?: string[], thisRow?: boolean, specialItem?: ParsedToken["specialItem"]) => SelectionWithSheetName | undefined;
|
|
7
|
+
export type SelectionFromNamedRangeResolver = (variableName: string) => SelectionWithSheetName | unknown;
|
|
8
|
+
export declare const isSelection: (sel: unknown) => sel is SelectionWithSheetName;
|
|
9
|
+
export declare const MAX_ROW_COUNT = 1048576;
|
|
10
|
+
export declare const MAX_COLUMN_COUNT = 16384;
|
|
11
|
+
export declare const DEFAULT_ROW_COUNT = 1000;
|
|
12
|
+
export declare const DEFAULT_COLUMN_COUNT = 100;
|
|
13
|
+
export declare const DEFAULT_ROW_HEIGHT = 21;
|
|
14
|
+
export declare const DEFAULT_COLUMN_WIDTH = 100;
|
|
15
|
+
export declare const DEFAULT_ACTIVE_CELL: CellInterface;
|
|
16
|
+
export declare const addressToSelection: (address: string, rowCount?: number, columnCount?: number) => SelectionArea | undefined;
|
|
17
|
+
export declare const addressToSheetRange: (address: string, sheetId: number, rowCount?: number, columnCount?: number) => SheetRange | undefined;
|
|
18
|
+
/**
|
|
19
|
+
* Convert a selection object to A1 address form (optionally sheet-qualified).
|
|
20
|
+
*/
|
|
21
|
+
export declare const selectionToAddress: (selection: SelectionArea | undefined, sheetName?: string, merges?: GridRange[] | null, rowCount?: number, columnCount?: number) => string | undefined;
|
|
22
|
+
export declare const checkIfStringIsReference: (value: string | undefined) => boolean;
|
|
23
|
+
export declare const generateSelectionsFromFormula: (value: string, _sheetId?: number, currentSheetName?: string, activeCell?: CellInterface, rowCount?: number, columnCount?: number, tokenizer?: (value: string | undefined, sheetName?: string) => ParsedToken[], getSelectionFromTable?: SelectionFromTableResolver, getSelectionFromNamedRange?: SelectionFromNamedRangeResolver) => SelectionWithSheetName[];
|
|
24
|
+
/**
|
|
25
|
+
* Check if two ranges are equal.
|
|
26
|
+
*/
|
|
27
|
+
export declare const isAreasEqual: (area1: GridRange | undefined, area2: GridRange | undefined) => boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Check if a cell is inside the provided range bounds.
|
|
30
|
+
*/
|
|
31
|
+
export declare const isCellWithinBounds: (cell: CellInterface, bounds?: GridRange) => boolean;
|
|
32
|
+
/**
|
|
33
|
+
* Return a single-cell selection for the active cell.
|
|
34
|
+
*/
|
|
35
|
+
export declare const selectionFromActiveCell: (activeCell: CellInterface | null) => SelectionArea[];
|
|
36
|
+
/**
|
|
37
|
+
* Check if two ranges overlap.
|
|
38
|
+
*/
|
|
39
|
+
export declare const areaIntersects: (area1: GridRange, area2: GridRange | undefined) => boolean;
|
|
40
|
+
/**
|
|
41
|
+
* Check if `needle` range is fully inside `haystack`.
|
|
42
|
+
*/
|
|
43
|
+
export declare const areaInsideArea: (needle: GridRange, haystack: GridRange) => boolean;
|
|
44
|
+
/**
|
|
45
|
+
* Intersection of two ranges, or `null` when they don't overlap.
|
|
46
|
+
*/
|
|
47
|
+
export declare const areaIntersection: (area1: GridRange, area2: GridRange) => GridRange | null;
|
|
48
|
+
/**
|
|
49
|
+
* Re-scope a rule's ranges after a drag-move of `from` → `to` on `sheetId`, so
|
|
50
|
+
* range-scoped rules (conditional formats, data validations) travel with the
|
|
51
|
+
* moved cells the way Excel and Google Sheets carry them:
|
|
52
|
+
*
|
|
53
|
+
* - a range on another sheet, or untouched by the move, is kept as-is
|
|
54
|
+
* - a range that travelled entirely inside the moved area is shifted with it
|
|
55
|
+
* - a range that only partially overlaps the moved area keeps its original
|
|
56
|
+
* scope (the source cells left behind keep the rule) and gains the shifted
|
|
57
|
+
* overlap at the destination — unless one of the rule's ranges already
|
|
58
|
+
* covers that destination
|
|
59
|
+
*
|
|
60
|
+
* Returns the new ranges, or `null` when the move leaves every range
|
|
61
|
+
* untouched, so callers can skip the rule update entirely (no history entry,
|
|
62
|
+
* no re-evaluation).
|
|
63
|
+
*/
|
|
64
|
+
export declare const rescopeSheetRangesOnMove: (ranges: readonly SheetRange[], sheetId: number, from: GridRange, to: GridRange) => SheetRange[] | null;
|
|
65
|
+
/**
|
|
66
|
+
* Convert a cell coordinate into a single-cell range.
|
|
67
|
+
*/
|
|
68
|
+
export declare const cellToArea: ({ rowIndex, columnIndex, }: CellInterface) => GridRange;
|
|
69
|
+
/**
|
|
70
|
+
* Convert font size from points to pixels.
|
|
71
|
+
*/
|
|
72
|
+
export declare const pointToPixel: (pt: number) => number;
|
|
73
|
+
/**
|
|
74
|
+
* Convert font size from pixels to points.
|
|
75
|
+
*/
|
|
76
|
+
export declare const pixelToPoint: (px: number) => number;
|
|
77
|
+
/**
|
|
78
|
+
* Resolve a table column name to absolute column index.
|
|
79
|
+
*/
|
|
80
|
+
export declare const getColumnIndexFromColumnName: (table: {
|
|
81
|
+
columns?: Array<{
|
|
82
|
+
name?: string | null;
|
|
83
|
+
}> | null;
|
|
84
|
+
range: {
|
|
85
|
+
startColumnIndex: number;
|
|
86
|
+
};
|
|
87
|
+
}, columnName: string | undefined) => number | undefined;
|
|
88
|
+
//# sourceMappingURL=selection.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"selection.d.ts","sourceRoot":"","sources":["../../selection.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
import { desanitizeSheetName } from "./string";
|
|
2
|
+
import { isNil } from "./typeguards";
|
|
3
|
+
import { addressToCell, cellToAddress, sanitizeSheetName, selectionBoundsFromCells, } from "./string";
|
|
4
|
+
export const isSelection = (sel) => {
|
|
5
|
+
return !!sel?.range;
|
|
6
|
+
};
|
|
7
|
+
export const MAX_ROW_COUNT = 1_048_576;
|
|
8
|
+
export const MAX_COLUMN_COUNT = 16_384;
|
|
9
|
+
export const DEFAULT_ROW_COUNT = 1000;
|
|
10
|
+
export const DEFAULT_COLUMN_COUNT = 100;
|
|
11
|
+
export const DEFAULT_ROW_HEIGHT = 21;
|
|
12
|
+
export const DEFAULT_COLUMN_WIDTH = 100;
|
|
13
|
+
export const DEFAULT_ACTIVE_CELL = Object.freeze({
|
|
14
|
+
rowIndex: 1,
|
|
15
|
+
columnIndex: 1,
|
|
16
|
+
});
|
|
17
|
+
export const addressToSelection = (address, rowCount = MAX_ROW_COUNT, columnCount = MAX_COLUMN_COUNT) => {
|
|
18
|
+
let [from, to] = address.split(":");
|
|
19
|
+
if (!from)
|
|
20
|
+
return undefined;
|
|
21
|
+
if (!to)
|
|
22
|
+
to = from;
|
|
23
|
+
const fromCell = addressToCell(from, {
|
|
24
|
+
position: "start",
|
|
25
|
+
rowCount,
|
|
26
|
+
columnCount,
|
|
27
|
+
});
|
|
28
|
+
const toCell = addressToCell(to, {
|
|
29
|
+
position: "end",
|
|
30
|
+
rowCount,
|
|
31
|
+
columnCount,
|
|
32
|
+
});
|
|
33
|
+
if (!fromCell || !toCell)
|
|
34
|
+
return undefined;
|
|
35
|
+
return { range: selectionBoundsFromCells(fromCell, toCell) };
|
|
36
|
+
};
|
|
37
|
+
export const addressToSheetRange = (address, sheetId, rowCount = MAX_ROW_COUNT, columnCount = MAX_COLUMN_COUNT) => {
|
|
38
|
+
const selection = addressToSelection(address, rowCount, columnCount);
|
|
39
|
+
if (!selection) {
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
sheetId,
|
|
44
|
+
...selection.range,
|
|
45
|
+
};
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* Convert a selection object to A1 address form (optionally sheet-qualified).
|
|
49
|
+
*/
|
|
50
|
+
export const selectionToAddress = (selection, sheetName, merges = [], rowCount = MAX_ROW_COUNT, columnCount = MAX_COLUMN_COUNT) => {
|
|
51
|
+
if (!selection) {
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
const mergedCell = merges?.find((merge) => isAreasEqual(selection.range, merge));
|
|
55
|
+
const { startRowIndex, startColumnIndex, endRowIndex, endColumnIndex } = mergedCell
|
|
56
|
+
? cellToArea({
|
|
57
|
+
rowIndex: mergedCell.startRowIndex,
|
|
58
|
+
columnIndex: mergedCell.startColumnIndex,
|
|
59
|
+
})
|
|
60
|
+
: selection.range;
|
|
61
|
+
const isFullColumn = startRowIndex === 1 && endRowIndex === rowCount;
|
|
62
|
+
const isFullRow = startColumnIndex === 1 && endColumnIndex === columnCount;
|
|
63
|
+
const from = cellToAddress({
|
|
64
|
+
rowIndex: startRowIndex,
|
|
65
|
+
columnIndex: startColumnIndex,
|
|
66
|
+
}, undefined, undefined, isFullColumn, isFullRow);
|
|
67
|
+
const to = cellToAddress({
|
|
68
|
+
rowIndex: endRowIndex,
|
|
69
|
+
columnIndex: endColumnIndex,
|
|
70
|
+
}, undefined, undefined, isFullColumn, isFullRow);
|
|
71
|
+
const cellAddress = from === to && !isFullColumn && !isFullRow ? from : `${from}:${to || ""}`;
|
|
72
|
+
return `${sheetName ? sanitizeSheetName(sheetName) + "!" : ""}${cellAddress}`;
|
|
73
|
+
};
|
|
74
|
+
export const checkIfStringIsReference = (value) => {
|
|
75
|
+
if (!value)
|
|
76
|
+
return false;
|
|
77
|
+
const v = String(value).trim();
|
|
78
|
+
const expr = v.startsWith("=") ? v.slice(1) : v;
|
|
79
|
+
const refRegex = /^(?:[^!]+!)?\$?[A-Z]+\$?\d+(?::\$?[A-Z]+\$?\d+)?$/i;
|
|
80
|
+
return refRegex.test(expr);
|
|
81
|
+
};
|
|
82
|
+
export const generateSelectionsFromFormula = (value, _sheetId, currentSheetName, activeCell, rowCount = MAX_ROW_COUNT, columnCount = MAX_COLUMN_COUNT, tokenizer, getSelectionFromTable, getSelectionFromNamedRange) => {
|
|
83
|
+
const selections = [];
|
|
84
|
+
if (!value)
|
|
85
|
+
return selections;
|
|
86
|
+
if (tokenizer && !isNil(_sheetId)) {
|
|
87
|
+
const sheetId = _sheetId;
|
|
88
|
+
const tokens = tokenizer(value, currentSheetName) ?? [];
|
|
89
|
+
for (const token of tokens) {
|
|
90
|
+
if (token.structured && getSelectionFromTable) {
|
|
91
|
+
const structuredSelection = getSelectionFromTable(sheetId, activeCell, token.tableName, token.columnNames, token.thisRow, token.specialItem);
|
|
92
|
+
if (structuredSelection && isSelection(structuredSelection)) {
|
|
93
|
+
selections.push(structuredSelection);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (token.isNamedRange && getSelectionFromNamedRange) {
|
|
97
|
+
const namedRangeSelection = getSelectionFromNamedRange(token.image);
|
|
98
|
+
if (namedRangeSelection && isSelection(namedRangeSelection)) {
|
|
99
|
+
selections.push(namedRangeSelection);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (token.selection) {
|
|
103
|
+
const address = token.image.slice(token.image.indexOf("!") + 1);
|
|
104
|
+
const selectionRange = addressToSelection(address, rowCount, columnCount);
|
|
105
|
+
if (selectionRange) {
|
|
106
|
+
selections.push({
|
|
107
|
+
...selectionRange,
|
|
108
|
+
sheetName: desanitizeSheetName(token.sheetName ?? currentSheetName),
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return selections;
|
|
114
|
+
}
|
|
115
|
+
const input = value.startsWith("=") ? value.slice(1) : value;
|
|
116
|
+
const refPattern = /((?:'[^']+'|[^'!,]+)!)?\$?[A-Z]+\$?\d+(?::\$?[A-Z]+\$?\d+)?/g;
|
|
117
|
+
let m;
|
|
118
|
+
while ((m = refPattern.exec(input))) {
|
|
119
|
+
const token = m[0];
|
|
120
|
+
const [maybeSheet, addr] = token.includes("!")
|
|
121
|
+
? [
|
|
122
|
+
token.slice(0, token.indexOf("!")),
|
|
123
|
+
token.slice(token.indexOf("!") + 1),
|
|
124
|
+
]
|
|
125
|
+
: [undefined, token];
|
|
126
|
+
const sel = addressToSelection(addr, rowCount, columnCount);
|
|
127
|
+
if (sel) {
|
|
128
|
+
selections.push({
|
|
129
|
+
...sel,
|
|
130
|
+
sheetName: maybeSheet
|
|
131
|
+
? maybeSheet.replace(/!$/, "").replace(/^'|'$/g, "") || undefined
|
|
132
|
+
: currentSheetName,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return selections;
|
|
137
|
+
};
|
|
138
|
+
/**
|
|
139
|
+
* Check if two ranges are equal.
|
|
140
|
+
*/
|
|
141
|
+
export const isAreasEqual = (area1, area2) => {
|
|
142
|
+
if (area1 === undefined || area2 === undefined) {
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
return (area1.endRowIndex === area2.endRowIndex &&
|
|
146
|
+
area1.startRowIndex === area2.startRowIndex &&
|
|
147
|
+
area1.startColumnIndex === area2.startColumnIndex &&
|
|
148
|
+
area1.endColumnIndex === area2.endColumnIndex);
|
|
149
|
+
};
|
|
150
|
+
/**
|
|
151
|
+
* Check if a cell is inside the provided range bounds.
|
|
152
|
+
*/
|
|
153
|
+
export const isCellWithinBounds = (cell, bounds) => {
|
|
154
|
+
if (!bounds) {
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
if (cell.rowIndex < bounds.startRowIndex ||
|
|
158
|
+
cell.rowIndex > bounds.endRowIndex) {
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
if (cell.columnIndex < bounds.startColumnIndex ||
|
|
162
|
+
cell.columnIndex > bounds.endColumnIndex) {
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
return true;
|
|
166
|
+
};
|
|
167
|
+
/**
|
|
168
|
+
* Return a single-cell selection for the active cell.
|
|
169
|
+
*/
|
|
170
|
+
export const selectionFromActiveCell = (activeCell) => {
|
|
171
|
+
if (!activeCell)
|
|
172
|
+
return [];
|
|
173
|
+
return [
|
|
174
|
+
{
|
|
175
|
+
range: {
|
|
176
|
+
startRowIndex: activeCell.rowIndex,
|
|
177
|
+
startColumnIndex: activeCell.columnIndex,
|
|
178
|
+
endRowIndex: activeCell.rowIndex,
|
|
179
|
+
endColumnIndex: activeCell.columnIndex,
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
];
|
|
183
|
+
};
|
|
184
|
+
/**
|
|
185
|
+
* Check if two ranges overlap.
|
|
186
|
+
*/
|
|
187
|
+
export const areaIntersects = (area1, area2) => {
|
|
188
|
+
if (!area2) {
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
if (area1.startColumnIndex > area2.endColumnIndex ||
|
|
192
|
+
area2.startColumnIndex > area1.endColumnIndex) {
|
|
193
|
+
return false;
|
|
194
|
+
}
|
|
195
|
+
if (area1.startRowIndex > area2.endRowIndex ||
|
|
196
|
+
area2.startRowIndex > area1.endRowIndex) {
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
return true;
|
|
200
|
+
};
|
|
201
|
+
/**
|
|
202
|
+
* Check if `needle` range is fully inside `haystack`.
|
|
203
|
+
*/
|
|
204
|
+
export const areaInsideArea = (needle, haystack) => {
|
|
205
|
+
return (needle.startRowIndex >= haystack.startRowIndex &&
|
|
206
|
+
needle.endRowIndex <= haystack.endRowIndex &&
|
|
207
|
+
needle.startColumnIndex >= haystack.startColumnIndex &&
|
|
208
|
+
needle.endColumnIndex <= haystack.endColumnIndex);
|
|
209
|
+
};
|
|
210
|
+
/**
|
|
211
|
+
* Intersection of two ranges, or `null` when they don't overlap.
|
|
212
|
+
*/
|
|
213
|
+
export const areaIntersection = (area1, area2) => {
|
|
214
|
+
const startRowIndex = Math.max(area1.startRowIndex, area2.startRowIndex);
|
|
215
|
+
const endRowIndex = Math.min(area1.endRowIndex, area2.endRowIndex);
|
|
216
|
+
const startColumnIndex = Math.max(area1.startColumnIndex, area2.startColumnIndex);
|
|
217
|
+
const endColumnIndex = Math.min(area1.endColumnIndex, area2.endColumnIndex);
|
|
218
|
+
if (startRowIndex > endRowIndex || startColumnIndex > endColumnIndex) {
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
return { startRowIndex, endRowIndex, startColumnIndex, endColumnIndex };
|
|
222
|
+
};
|
|
223
|
+
/**
|
|
224
|
+
* Re-scope a rule's ranges after a drag-move of `from` → `to` on `sheetId`, so
|
|
225
|
+
* range-scoped rules (conditional formats, data validations) travel with the
|
|
226
|
+
* moved cells the way Excel and Google Sheets carry them:
|
|
227
|
+
*
|
|
228
|
+
* - a range on another sheet, or untouched by the move, is kept as-is
|
|
229
|
+
* - a range that travelled entirely inside the moved area is shifted with it
|
|
230
|
+
* - a range that only partially overlaps the moved area keeps its original
|
|
231
|
+
* scope (the source cells left behind keep the rule) and gains the shifted
|
|
232
|
+
* overlap at the destination — unless one of the rule's ranges already
|
|
233
|
+
* covers that destination
|
|
234
|
+
*
|
|
235
|
+
* Returns the new ranges, or `null` when the move leaves every range
|
|
236
|
+
* untouched, so callers can skip the rule update entirely (no history entry,
|
|
237
|
+
* no re-evaluation).
|
|
238
|
+
*/
|
|
239
|
+
export const rescopeSheetRangesOnMove = (ranges, sheetId, from, to) => {
|
|
240
|
+
const rowDelta = to.startRowIndex - from.startRowIndex;
|
|
241
|
+
const columnDelta = to.startColumnIndex - from.startColumnIndex;
|
|
242
|
+
if (rowDelta === 0 && columnDelta === 0) {
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
const shift = (range) => ({
|
|
246
|
+
...range,
|
|
247
|
+
startRowIndex: range.startRowIndex + rowDelta,
|
|
248
|
+
endRowIndex: range.endRowIndex + rowDelta,
|
|
249
|
+
startColumnIndex: range.startColumnIndex + columnDelta,
|
|
250
|
+
endColumnIndex: range.endColumnIndex + columnDelta,
|
|
251
|
+
});
|
|
252
|
+
let changed = false;
|
|
253
|
+
const next = [];
|
|
254
|
+
const shiftedOverlaps = [];
|
|
255
|
+
for (const range of ranges) {
|
|
256
|
+
if (range.sheetId !== sheetId) {
|
|
257
|
+
next.push(range);
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
const overlap = areaIntersection(range, from);
|
|
261
|
+
if (!overlap) {
|
|
262
|
+
next.push(range);
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
if (areaInsideArea(range, from)) {
|
|
266
|
+
next.push(shift(range));
|
|
267
|
+
changed = true;
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
next.push(range);
|
|
271
|
+
shiftedOverlaps.push(shift({ ...overlap, sheetId: range.sheetId }));
|
|
272
|
+
}
|
|
273
|
+
for (const shifted of shiftedOverlaps) {
|
|
274
|
+
const covered = next.some((range) => range.sheetId === sheetId && areaInsideArea(shifted, range));
|
|
275
|
+
if (covered) {
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
next.push(shifted);
|
|
279
|
+
changed = true;
|
|
280
|
+
}
|
|
281
|
+
return changed ? next : null;
|
|
282
|
+
};
|
|
283
|
+
/**
|
|
284
|
+
* Convert a cell coordinate into a single-cell range.
|
|
285
|
+
*/
|
|
286
|
+
export const cellToArea = ({ rowIndex, columnIndex, }) => {
|
|
287
|
+
return {
|
|
288
|
+
startRowIndex: rowIndex,
|
|
289
|
+
startColumnIndex: columnIndex,
|
|
290
|
+
endColumnIndex: columnIndex,
|
|
291
|
+
endRowIndex: rowIndex,
|
|
292
|
+
};
|
|
293
|
+
};
|
|
294
|
+
/**
|
|
295
|
+
* Convert font size from points to pixels.
|
|
296
|
+
*/
|
|
297
|
+
export const pointToPixel = (pt) => Math.round((13 / 10) * pt);
|
|
298
|
+
/**
|
|
299
|
+
* Convert font size from pixels to points.
|
|
300
|
+
*/
|
|
301
|
+
export const pixelToPoint = (px) => Math.round((px / 13) * 10);
|
|
302
|
+
/**
|
|
303
|
+
* Resolve a table column name to absolute column index.
|
|
304
|
+
*/
|
|
305
|
+
export const getColumnIndexFromColumnName = (table, columnName) => {
|
|
306
|
+
const fromIndex = table.columns?.findIndex((col) => col.name === columnName);
|
|
307
|
+
if (fromIndex == null || fromIndex === -1) {
|
|
308
|
+
return undefined;
|
|
309
|
+
}
|
|
310
|
+
return table.range.startColumnIndex + fromIndex;
|
|
311
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { TextFormatRun } from "@rowsncolumns/common-types";
|
|
2
|
+
/**
|
|
3
|
+
* Deterministic string form of a (text, runs) pair, suitable as input to
|
|
4
|
+
* `contentHash` for rich shared-string entries.
|
|
5
|
+
*
|
|
6
|
+
* Two rich entries that render identically produce the same canonical string
|
|
7
|
+
* regardless of property ordering or absent-vs-false boolean variations. The
|
|
8
|
+
* leading `RICH_MARKER` (control characters) guarantees a rich canonical can
|
|
9
|
+
* never equal any plain shared-string text, so plain↔rich never collide on
|
|
10
|
+
* the downstream content hash.
|
|
11
|
+
*
|
|
12
|
+
* IMPORTANT: This is the dedup key for shared-string IDs across sessions.
|
|
13
|
+
* Changing the format, key order, or stripping rules invalidates every
|
|
14
|
+
* existing rich-string ID. Mirror updates here in any other-language port
|
|
15
|
+
* (e.g. Python) used in the same collaboration session.
|
|
16
|
+
*/
|
|
17
|
+
export declare const canonicalizeRichSharedString: (text: string, runs: readonly TextFormatRun[]) => string;
|
|
18
|
+
//# sourceMappingURL=shared-string-canonical.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shared-string-canonical.d.ts","sourceRoot":"","sources":["../../shared-string-canonical.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAIV,aAAa,EACd,MAAM,4BAA4B,CAAC;AAkDpC;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,4BAA4B,GACvC,MAAM,MAAM,EACZ,MAAM,SAAS,aAAa,EAAE,KAC7B,MAGF,CAAC"}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// U+0001 (START OF HEADING). Cell text shouldn't contain control chars, so
|
|
2
|
+
// using it as a separator and as the `RICH_MARKER` prefix guarantees the rich
|
|
3
|
+
// canonical string can never collide with any plain shared-string value.
|
|
4
|
+
// Built via fromCharCode rather than a string literal so the control byte is
|
|
5
|
+
// visible in source.
|
|
6
|
+
const SEP = String.fromCharCode(1);
|
|
7
|
+
const RICH_MARKER = `${SEP}RICH${SEP}`;
|
|
8
|
+
const canonicalizeColor = (color) => {
|
|
9
|
+
if (color == null)
|
|
10
|
+
return "";
|
|
11
|
+
if (typeof color === "string")
|
|
12
|
+
return `s:${color}`;
|
|
13
|
+
const tint = color.tint == null ? "" : color.tint;
|
|
14
|
+
return `t:${color.theme}:${tint}`;
|
|
15
|
+
};
|
|
16
|
+
// Fixed key order. Skips null/undefined and false-equivalent booleans so a
|
|
17
|
+
// producer that writes `{ bold: true }` hashes identically to one that writes
|
|
18
|
+
// `{ bold: true, italic: false }`. Any new field added to `TextFormat` MUST be
|
|
19
|
+
// appended here, otherwise rich entries differing only in the new field will
|
|
20
|
+
// collapse and dedup incorrectly.
|
|
21
|
+
const canonicalizeFormat = (format) => {
|
|
22
|
+
if (!format)
|
|
23
|
+
return "";
|
|
24
|
+
const parts = [];
|
|
25
|
+
if (format.bold)
|
|
26
|
+
parts.push("b");
|
|
27
|
+
if (format.italic)
|
|
28
|
+
parts.push("i");
|
|
29
|
+
if (format.strikethrough)
|
|
30
|
+
parts.push("s");
|
|
31
|
+
if (format.underline)
|
|
32
|
+
parts.push("u");
|
|
33
|
+
if (format.color != null)
|
|
34
|
+
parts.push(`c=${canonicalizeColor(format.color)}`);
|
|
35
|
+
if (format.fontFamily)
|
|
36
|
+
parts.push(`ff=${format.fontFamily}`);
|
|
37
|
+
if (format.fontSize != null)
|
|
38
|
+
parts.push(`fs=${format.fontSize}`);
|
|
39
|
+
if (format.baselineOffset)
|
|
40
|
+
parts.push(`bo=${format.baselineOffset}`);
|
|
41
|
+
return parts.join(",");
|
|
42
|
+
};
|
|
43
|
+
const canonicalizeMention = (mention) => `v=${mention.value},l=${mention.label}`;
|
|
44
|
+
const canonicalizeRun = (run) => {
|
|
45
|
+
if (run.nodeType === "mention") {
|
|
46
|
+
return `M:${run.startIndex}:${run.endIndex}:${canonicalizeMention(run.mention)}`;
|
|
47
|
+
}
|
|
48
|
+
return `T:${run.startIndex}:${run.endIndex}:${canonicalizeFormat(run.format)}`;
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* Deterministic string form of a (text, runs) pair, suitable as input to
|
|
52
|
+
* `contentHash` for rich shared-string entries.
|
|
53
|
+
*
|
|
54
|
+
* Two rich entries that render identically produce the same canonical string
|
|
55
|
+
* regardless of property ordering or absent-vs-false boolean variations. The
|
|
56
|
+
* leading `RICH_MARKER` (control characters) guarantees a rich canonical can
|
|
57
|
+
* never equal any plain shared-string text, so plain↔rich never collide on
|
|
58
|
+
* the downstream content hash.
|
|
59
|
+
*
|
|
60
|
+
* IMPORTANT: This is the dedup key for shared-string IDs across sessions.
|
|
61
|
+
* Changing the format, key order, or stripping rules invalidates every
|
|
62
|
+
* existing rich-string ID. Mirror updates here in any other-language port
|
|
63
|
+
* (e.g. Python) used in the same collaboration session.
|
|
64
|
+
*/
|
|
65
|
+
export const canonicalizeRichSharedString = (text, runs) => {
|
|
66
|
+
const runsKey = runs.map(canonicalizeRun).join(";");
|
|
67
|
+
return `${RICH_MARKER}${text}${SEP}${runsKey}`;
|
|
68
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export type BaseSheet = {
|
|
2
|
+
index?: number | null;
|
|
3
|
+
title: string;
|
|
4
|
+
hidden?: boolean | null;
|
|
5
|
+
sheetId: number;
|
|
6
|
+
} & Record<string, any>;
|
|
7
|
+
/**
|
|
8
|
+
* Sort sheets by index
|
|
9
|
+
* @param sheets
|
|
10
|
+
* @returns
|
|
11
|
+
*/
|
|
12
|
+
export declare const sortSheetsByIndex: <S extends BaseSheet>(sheets: S[], includeHidden?: boolean) => (S & {
|
|
13
|
+
index: number;
|
|
14
|
+
})[];
|
|
15
|
+
//# sourceMappingURL=sort.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sort.d.ts","sourceRoot":"","sources":["../../sort.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,SAAS,GAAG;IACtB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC;IACxB,OAAO,EAAE,MAAM,CAAC;CACjB,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;AACxB;;;;GAIG;AACH,eAAO,MAAM,iBAAiB,GAAI,CAAC,SAAS,SAAS,EACnD,QAAQ,CAAC,EAAE,EACX,gBAAgB,OAAO;;IA0CxB,CAAC"}
|