UNPKG

@odoo/o-spreadsheet

Version:
1,560 lines (1,549 loc) 2.95 MB
/** * This file is generated by o-spreadsheet build tools. Do not edit it. * @see https://github.com/odoo/o-spreadsheet * @version 19.3.4 * @date 2026-05-15T07:07:34.417Z * @hash 1dc7b42 */ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); //#region \0rolldown/runtime.js var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __commonJSMin = (cb, mod) => () => (mod || (cb((mod = { exports: {} }).exports, mod), cb = null), mod.exports); var __exportAll = (all, no_symbols) => { let target = {}; for (var name in all) { __defProp(target, name, { get: all[name], enumerable: true }); } if (!no_symbols) { __defProp(target, Symbol.toStringTag, { value: "Module" }); } return target; }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) { key = keys[i]; if (!__hasOwnProp.call(to, key) && key !== except) { __defProp(to, key, { get: ((k) => from[k]).bind(null, key), enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } } } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod)); //#endregion let _odoo_owl = require("@odoo/owl"); //#region src/dom_mock.ts if (typeof globalThis.OffscreenCanvas === "undefined") { class MockOffscreenCanvasRenderingContext2D { constructor() { return proxy(this); } save() {} restore() {} measureText(text) { return { width: text.length }; } } class MockOffscreenCanvas { constructor(width, height) { return proxy(this); } getContext(contextId) { if (contextId === "2d") return new MockOffscreenCanvasRenderingContext2D(); return null; } } function proxy(target) { return new Proxy(target, { get: function(obj, prop, receiver) { if (Reflect.has(obj, prop)) return Reflect.get(obj, prop, receiver); throw new Error(`OffscreenCanvas mock: "${String(prop)}" is not implemented.\nAdd it to MockOffscreenCanvas or MockOffscreenCanvasRenderingContext2D if needed.`); } }); } globalThis.OffscreenCanvas = MockOffscreenCanvas; } if (typeof globalThis.DOMParser === "undefined") globalThis.DOMParser = class DOMParser { parseFromString() { return { querySelector() { return null; }, querySelectorAll() { return []; }, body: {} }; } }; if (typeof globalThis.document === "undefined") { const noop = function() {}; globalThis.document = { implementation: { createDocument() { return {}; } }, addEventListener: noop, removeEventListener: noop, querySelectorAll() { return []; }, querySelector() { return null; }, createElement() { return { setAttribute() {}, getBoundingClientRect() { return { width: 0, height: 0 }; } }; }, createTextNode() { return {}; }, cookie: "", head: { querySelectorAll() { return []; } }, body: { classList: { add: noop, remove: noop, contains: noop }, contains() { return false; }, appendChild() {}, removeChild() {} } }; } //#endregion //#region src/actions/action.ts function createActions(menuItems) { return menuItems.map(createAction).sort((a, b) => a.sequence - b.sequence); } let nextItemId = 1; function createAction(item) { const name = item.name; const children = item.children; const description = item.description; const shortcut = item.shortcut; const icon = item.icon; const secondaryIcon = item.secondaryIcon; const itemId = item.id || nextItemId++; const isEnabled = item.isEnabled ? item.isEnabled : () => true; return { id: itemId.toString(), name: typeof name === "function" ? name : () => name, isVisible: item.isVisible ? item.isVisible : () => true, isEnabled, isActive: item.isActive, execute: item.execute ? (env, isMiddleClick) => { if (isEnabled(env)) return item.execute(env, isMiddleClick); } : void 0, children: children ? (env) => { return children.map((child) => typeof child === "function" ? child(env) : child).flat().map(createAction).sort((a, b) => a.sequence - b.sequence); } : () => [], isReadonlyAllowed: item.isReadonlyAllowed || false, isEnabledOnLockedSheet: item.isEnabledOnLockedSheet || false, separator: item.separator || false, icon: typeof icon === "function" ? icon : () => icon || "", iconColor: item.iconColor, secondaryIcon: typeof secondaryIcon === "function" ? secondaryIcon : () => secondaryIcon || "", description: typeof description === "function" ? description : () => description || "", shortcut: shortcut || "", textColor: item.textColor, sequence: item.sequence || 0, onStartHover: item.onStartHover, onStopHover: item.onStopHover }; } function getMenuItemsAndSeparators(env, actions) { const menuItemsAndSeparators = []; for (let i = 0; i < actions.length; i++) { const menuItem = actions[i]; if (menuItem.isVisible(env) && (!isRootMenu(menuItem) || hasVisibleChildren(env, menuItem))) menuItemsAndSeparators.push(menuItem); if (menuItem.separator && i !== actions.length - 1 && menuItemsAndSeparators[menuItemsAndSeparators.length - 1] !== "separator") menuItemsAndSeparators.push("separator"); } if (menuItemsAndSeparators[menuItemsAndSeparators.length - 1] === "separator") menuItemsAndSeparators.pop(); if (menuItemsAndSeparators.length === 1 && menuItemsAndSeparators[0] === "separator") return []; return menuItemsAndSeparators; } function isRootMenu(menu) { return !menu.execute; } function hasVisibleChildren(env, menu) { return menu.children(env).some((child) => child.isVisible(env)); } function isMenuItemEnabled(env, menu) { const children = menu.children?.(env); if (children.length) return children.some((child) => isMenuItemEnabled(env, child)); else { if (menu.isEnabled(env)) return env.model.getters.isReadonly() ? menu.isReadonlyAllowed : true; return false; } } //#endregion //#region src/constants.ts const CANVAS_SHIFT = .5; const HIGHLIGHT_COLOR = "#017E84"; const BACKGROUND_HEADER_COLOR = "#F8F9FA"; const BACKGROUND_HEADER_SELECTED_COLOR = "#E8EAED"; const BACKGROUND_HEADER_ACTIVE_COLOR = "#595959"; const TEXT_HEADER_COLOR = "#666666"; const SELECTION_BORDER_COLOR = "#3266ca"; const HEADER_BORDER_COLOR = "#C0C0C0"; const CELL_BORDER_COLOR = "#E2E3E3"; const BACKGROUND_CHART_COLOR = "#FFFFFF"; const DEFAULT_COLOR_SCALE_MIDPOINT_COLOR = 11982760; const LINK_COLOR = HIGHLIGHT_COLOR; const FILTERS_COLOR = "#188038"; const FROZEN_PANE_HEADER_BORDER_COLOR = "#BCBCBC"; const FROZEN_PANE_BORDER_COLOR = "#DADFE8"; const COMPOSER_ASSISTANT_COLOR = "light-dark(#9B359B, #B972A6)"; const COLOR_TRANSPARENT = "#00000000"; const TABLE_HOVER_BACKGROUND_COLOR = "#017E8414"; const CHART_WATERFALL_POSITIVE_COLOR = "#4EA7F2"; const CHART_WATERFALL_NEGATIVE_COLOR = "#EA6175"; const CHART_WATERFALL_SUBTOTAL_COLOR = "#AAAAAA"; const GRAY_900 = "#111827"; const GRAY_400 = "#ced4da"; const GRAY_300 = "#D8DADD"; const GRAY_200 = "#E7E9ED"; const TEXT_BODY = "#374151"; const TEXT_BODY_MUTED = TEXT_BODY + "C2"; const ACTION_COLOR = HIGHLIGHT_COLOR; const CHART_TITLE_FONT_SIZE = 16; const DEFAULT_CHART_COLOR_SCALE = { minColor: "#FFF5EB", midColor: "#FD8D3C", maxColor: "#7F2704" }; const PIVOT_TOKEN_COLOR = "#F28C28"; const COLOR_PICKER_DEFAULTS = [ "#000000", "#434343", "#666666", "#999999", "#B7B7B7", "#CCCCCC", "#D9D9D9", "#EFEFEF", "#F3F3F3", "#FFFFFF", "#980000", "#FF0000", "#FF9900", "#FFFF00", "#00FF00", "#00FFFF", "#4A86E8", "#0000FF", "#9900FF", "#FF00FF", "#E6B8AF", "#F4CCCC", "#FCE5CD", "#FFF2CC", "#D9EAD3", "#D0E0E3", "#C9DAF8", "#CFE2F3", "#D9D2E9", "#EAD1DC", "#DD7E6B", "#EA9999", "#F9CB9C", "#FFE599", "#B6D7A8", "#A2C4C9", "#A4C2F4", "#9FC5E8", "#B4A7D6", "#D5A6BD", "#CC4125", "#E06666", "#F6B26B", "#FFD966", "#93C47D", "#76A5AF", "#6D9EEB", "#6FA8DC", "#8E7CC3", "#C27BA0", "#A61C00", "#CC0000", "#E69138", "#F1C232", "#6AA84F", "#45818E", "#3C78D8", "#3D85C6", "#674EA7", "#A64D79", "#85200C", "#990000", "#B45F06", "#BF9000", "#38761D", "#134F5C", "#1155CC", "#0B5394", "#351C75", "#741B47", "#5B0F00", "#660000", "#783F04", "#7F6000", "#274E13", "#0C343D", "#1C4587", "#073763", "#20124D", "#4C1130" ]; const DEFAULT_CELL_HEIGHT = 23; const FOOTER_HEIGHT = 2 * 23; const MENU_SEPARATOR_BORDER_WIDTH = 1; const MENU_SEPARATOR_PADDING = 5; const MENU_SEPARATOR_HEIGHT = 1 + 2 * 5; const ZOOM_VALUES = [ 50, 75, 100, 125, 150, 200 ]; const DEFAULT_STYLE = { align: "left", verticalAlign: "bottom", wrapping: "overflow", bold: false, italic: false, strikethrough: false, underline: false, fontSize: 10, fillColor: "", textColor: "", rotation: 0, hideGridLines: false }; const ROTATION_EPSILON = .001; const DEFAULT_NUMBER_STYLE = { ...DEFAULT_STYLE, align: "right" }; const DEFAULT_VERTICAL_ALIGN = DEFAULT_STYLE.verticalAlign; const DEFAULT_WRAPPING_MODE = DEFAULT_STYLE.wrapping; const DEFAULT_FONT_SIZE = DEFAULT_STYLE.fontSize; const DEFAULT_FONT = "'Roboto', arial"; const DEFAULT_BORDER_DESC = { style: "thin", color: "#000000" }; const DEFAULT_REVISION_ID = "START_REVISION"; const DEFAULT_GAUGE_LOWER_COLOR = "#EA6175"; const DEFAULT_GAUGE_MIDDLE_COLOR = "#FFD86D"; const DEFAULT_GAUGE_UPPER_COLOR = "#43C5B1"; const DEFAULT_SCORECARD_BASELINE_MODE = "difference"; const DEFAULT_SCORECARD_BASELINE_COLOR_UP = "#43C5B1"; const DEFAULT_SCORECARD_BASELINE_COLOR_DOWN = "#EA6175"; const LINE_FILL_TRANSPARENCY = .4; const FORBIDDEN_SHEETNAME_CHARS = [ "'", "*", "?", "/", "\\", "[", "]" ]; const FORBIDDEN_SHEETNAME_CHARS_IN_EXCEL_REGEX = /'|\*|\?|\/|\\|\[|\]/; let DEFAULT_SHEETVIEW_SIZE = 0; /** * The viewport dimensions are usually set by one of the components * (i.e. when grid component is mounted) to properly reflect its state in the DOM. * In the absence of a component (standalone model), is it mandatory to set reasonable default values * to ensure the correct operation. */ function getDefaultSheetViewSize() { return DEFAULT_SHEETVIEW_SIZE; } function setDefaultSheetViewSize(size) { DEFAULT_SHEETVIEW_SIZE = size; } const MAXIMAL_FREEZABLE_RATIO = .85; const FONT_SIZES = [ 6, 7, 8, 9, 10, 11, 12, 14, 18, 24, 36 ]; const PIVOT_STATIC_TABLE_CONFIG = { hasFilters: false, totalRow: false, firstColumn: true, lastColumn: false, numberOfHeaders: 1, bandedRows: true, bandedColumns: false, styleId: "TableStyleMedium5", automaticAutofill: false }; const PIVOT_INSERT_TABLE_STYLE_ID = "PivotTableStyleMedium12"; const PIVOT_MAX_NUMBER_OF_CELLS = 5e5; const DEFAULT_CURRENCY = { symbol: "$", position: "before", decimalPlaces: 2, code: "", name: "Dollar" }; const DEFAULT_CAROUSEL_TITLE_STYLE = { fontSize: 16, color: TEXT_BODY }; const DEFAULT_TOKEN_COLOR = "light-dark(#000000, #ffffff)"; const functionColor = DEFAULT_TOKEN_COLOR; const operatorColor = "#3da4ab"; const tokenColors = { OPERATOR: operatorColor, NUMBER: "#02c39a", STRING: "#00a82d", FUNCTION: functionColor, DEBUGGER: operatorColor, LEFT_PAREN: functionColor, RIGHT_PAREN: functionColor, ARG_SEPARATOR: functionColor, ORPHAN_RIGHT_PAREN: "#ff0000" }; //#endregion //#region src/types/misc.ts const borderStyles = [ "thin", "medium", "thick", "dashed", "dotted" ]; function isMatrix(x) { return Array.isArray(x) && Array.isArray(x[0]); } //#endregion //#region src/helpers/misc.ts const sanitizeSheetNameRegex = new RegExp(FORBIDDEN_SHEETNAME_CHARS_IN_EXCEL_REGEX, "g"); function isCloneable(obj) { return "clone" in obj && obj.clone instanceof Function; } /** * Escapes a string to use as a literal string in a RegExp. * @url https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#Escaping */ function escapeRegExp(str) { return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); } /** * Deep copy arrays, plain objects and primitive values. * Throws an error for other types such as class instances. * Sparse arrays remain sparse. */ function deepCopy(obj) { switch (typeof obj) { case "object": { if (obj === null) return obj; else if (isCloneable(obj)) return obj.clone(); else if (!(isPlainObject(obj) || obj instanceof Array)) throw new Error("Unsupported type: only objects and arrays are supported"); const result = Array.isArray(obj) ? new Array(obj.length) : {}; if (Array.isArray(obj)) { for (let i = 0, len = obj.length; i < len; i++) if (i in obj) result[i] = deepCopy(obj[i]); } else for (const key in obj) result[key] = deepCopy(obj[key]); return result; } case "number": case "string": case "boolean": case "function": case "undefined": return obj; default: throw new Error(`Unsupported type: ${typeof obj}`); } } /** * Check if the object is a plain old javascript object. */ function isPlainObject(obj) { return typeof obj === "object" && obj !== null && (obj?.constructor === Object || obj?.constructor === void 0); } /** * Sanitize the name of a sheet, by eventually removing quotes. */ function getUnquotedSheetName(sheetName) { return unquote(sheetName, "'"); } /** * Remove quotes from a quoted string. */ function unquote(string, quoteChar = "\"") { if (string.startsWith(quoteChar)) string = string.slice(1); if (string.endsWith(quoteChar)) string = string.slice(0, -1); return string; } /** * Add quotes around the sheet name or any symbol name if it contains at least one non alphanumeric character. */ function getCanonicalSymbolName(symbolName) { if (symbolName.match(/\w/g)?.length !== symbolName.length) symbolName = `'${symbolName}'`; return symbolName; } /** Replace the excel-excluded characters of a sheetName */ function sanitizeSheetName(sheetName, replacementChar = " ") { return sheetName.replace(sanitizeSheetNameRegex, replacementChar); } function clip(val, min, max) { return val < min ? min : val > max ? max : val; } /** * Create a range from start (included) to end (excluded). * range(10, 13) => [10, 11, 12] * range(2, 8, 2) => [2, 4, 6] */ function range(start, end, step = 1) { if (end <= start && step > 0) return []; if (step === 0) throw new Error("range() step must not be zero"); const length = Math.ceil(Math.abs((end - start) / step)); const array = Array(length); for (let i = 0; i < length; i++) array[i] = start + i * step; return array; } /** * Groups consecutive numbers. * The input array is assumed to be sorted * @param numbers */ function groupConsecutive(numbers) { return numbers.reduce((groups, currentRow, index, rows) => { if (Math.abs(currentRow - rows[index - 1]) === 1) groups[groups.length - 1].push(currentRow); else groups.push([currentRow]); return groups; }, []); } /** * Create one generator from two generators by linking * each item of the first generator to the next item of * the second generator. * * Let's say generator G1 yields A, B, C and generator G2 yields X, Y, Z. * The resulting generator of `linkNext(G1, G2)` will yield A', B', C' * where `A' = A & {next: Y}`, `B' = B & {next: Z}` and `C' = C & {next: undefined}` * @param generator * @param nextGenerator */ function* linkNext(generator, nextGenerator) { nextGenerator.next(); for (const item of generator) { const nextItem = nextGenerator.next(); yield { ...item, next: nextItem.done ? void 0 : nextItem.value }; } } function isBoolean(str) { const upperCased = str.toUpperCase(); return upperCased === "TRUE" || upperCased === "FALSE"; } const MARKDOWN_LINK_REGEX = /^\[(.+)\]\((.+)\)$/; const WEB_LINK_REGEX = /^https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,4}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)$/; function isMarkdownLink(str) { return MARKDOWN_LINK_REGEX.test(str); } /** * Check if the string is a web link. * e.g. http://odoo.com */ function isWebLink(str) { return WEB_LINK_REGEX.test(str); } /** * Build a markdown link from a label and an url */ function markdownLink(label, url) { return `[${label}](${url})`; } function parseMarkdownLink(str) { const matches = str.match(MARKDOWN_LINK_REGEX) || []; const label = matches[1]; const url = matches[2]; if (!label || !url) throw new Error(`Could not parse markdown link ${str}.`); return { label, url }; } const O_SPREADSHEET_LINK_PREFIX = "o-spreadsheet://"; function isSheetUrl(url) { return url.startsWith(O_SPREADSHEET_LINK_PREFIX); } function buildSheetLink(sheetId) { return `${O_SPREADSHEET_LINK_PREFIX}${sheetId}`; } /** * Parse a sheet link and return the sheet id */ function parseSheetUrl(sheetLink) { if (sheetLink.startsWith(O_SPREADSHEET_LINK_PREFIX)) return sheetLink.slice(16); throw new Error(`${sheetLink} is not a valid sheet link`); } /** * This helper function can be used as a type guard when filtering arrays. * const foo: number[] = [1, 2, undefined, 4].filter(isDefined) */ function isDefined(argument) { return argument !== void 0; } /** * Check if all the values of an object, and all the values of the objects inside of it, are undefined. */ function isObjectEmptyRecursive(argument) { if (argument === void 0) return true; return Object.values(argument).every((value) => typeof value === "object" ? isObjectEmptyRecursive(value) : !value); } /** * Returns a function, that, as long as it continues to be invoked, will not * be triggered. The function will be called after it stops being called for * N milliseconds. If `immediate` is passed, the function is called is called * immediately on the first call and the debouncing is triggered starting the second * call in the defined time window. * * Example: * debouncedFunction = debounce(() => console.log('Hello!'), 250); * debouncedFunction(); debouncedFunction(); // Will log 'Hello!' after 250ms * * debouncedFunction = debounce(() => console.log('Hello!'), 250, true); * debouncedFunction(); debouncedFunction(); // Will log 'Hello!' and relog it after 250ms * * * Also decorate the argument function with two methods: stopDebounce and isDebouncePending. * * Inspired by https://davidwalsh.name/javascript-debounce-function */ function debounce(func, wait, immediate) { let timeout = void 0; let firstCalled = false; const debounced = function() { const context = this; const args = Array.from(arguments); if (!firstCalled && immediate) { firstCalled = true; return func.apply(context, args); } function later() { timeout = void 0; firstCalled = false; func.apply(context, args); } clearTimeout(timeout); timeout = setTimeout(later, wait); }; debounced.isDebouncePending = () => timeout !== void 0; debounced.stopDebounce = () => { clearTimeout(timeout); }; return debounced; } /** * Creates a batched version of a callback so that all calls to it in the same * microtick will only call the original callback once. * * @param callback the callback to batch * @returns a batched version of the original callback * * Copied from odoo/owl repo. */ function batched(callback) { let scheduled = false; return async (...args) => { if (!scheduled) { scheduled = true; await Promise.resolve(); scheduled = false; callback(...args); } }; } /** Returns a copy of the function `callback` that can only be called * at most once every `delay` milliseconds. */ function throttle(callback, delay) { let lastCall = 0; return function(...args) { const now = Date.now(); if (now - lastCall >= delay) { lastCall = now; return callback(...args); } }; } function concat$1(chars) { let output = ""; for (let i = 0, len = chars.length; i < len; i++) output += chars[i]; return output; } /** * Lazy value computed by the provided function. */ function lazy(fn) { let isMemoized = false; let memo; const lazyValue = () => { if (!isMemoized) { memo = fn instanceof Function ? fn() : fn; isMemoized = true; } return memo; }; lazyValue.map = (callback) => lazy(() => callback(lazyValue())); return lazyValue; } /** * Find the next defined value after the given index in an array of strings. If there is no defined value * after the index, return the closest defined value before the index. Return an empty string if no * defined value was found. * */ function findNextDefinedValue(arr, index) { let value = arr.slice(index).find((val) => val); if (!value) value = arr.slice(0, index).reverse().find((val) => val); return value || ""; } /** Get index of first header added by an ADD_COLUMNS_ROWS command */ function getAddHeaderStartIndex(position, base) { return position === "after" ? base + 1 : base; } /** * Compares n objects. */ function deepEquals(...o) { if (o.length <= 1) return true; for (let index = 1; index < o.length; index++) if (!_deepEquals(o[0], o[index])) return false; return true; } function _deepEquals(o1, o2) { if (o1 === o2) return true; if (o1 && !o2 || o2 && !o1) return false; if (typeof o1 !== typeof o2) return false; if (typeof o1 !== "object") return false; for (const key in o2) if (!(key in o1) && o2[key] !== void 0) return false; for (const key in o1) { if (typeof o1[key] !== typeof o2[key]) return false; if (typeof o1[key] === "object") { if (!_deepEquals(o1[key], o2[key])) return false; } else if (o1[key] !== o2[key]) return false; } return true; } /** * Compares two arrays. * For performance reasons, this function is to be preferred * to 'deepEquals' in the case we know that the inputs are arrays. */ function deepEqualsArray(arr1, arr2) { if (arr1.length !== arr2.length) return false; for (let i = 0; i < arr1.length; i++) if (!deepEquals(arr1[i], arr2[i])) return false; return true; } /** * Check if the given array contains all the values of the other array. * It makes the assumption that both array do not contain duplicates. */ function includesAll(arr, values) { if (arr.length < values.length) return false; const set = new Set(arr); return values.every((value) => set.has(value)); } /** * Return an object with all the keys in the object that have a falsy value removed. */ function removeFalsyAttributes(obj) { if (!obj) return obj; const cleanObject = { ...obj }; Object.keys(cleanObject).forEach((key) => !cleanObject[key] && delete cleanObject[key]); return cleanObject; } /** * Equivalent to "\s" in regexp, minus the new lines characters * * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions/Character_Classes */ const specialWhiteSpaceSpecialCharacters = [ " ", "\f", "\v", String.fromCharCode(parseInt("00a0", 16)), String.fromCharCode(parseInt("1680", 16)), String.fromCharCode(parseInt("2000", 16)), String.fromCharCode(parseInt("200a", 16)), String.fromCharCode(parseInt("2028", 16)), String.fromCharCode(parseInt("2029", 16)), String.fromCharCode(parseInt("202f", 16)), String.fromCharCode(parseInt("205f", 16)), String.fromCharCode(parseInt("3000", 16)), String.fromCharCode(parseInt("feff", 16)) ]; const specialWhiteSpaceRegexp = new RegExp(specialWhiteSpaceSpecialCharacters.join("|"), "g"); const newLineRegexp = /(\r\n|\r)/g; const whiteSpaceCharacters = specialWhiteSpaceSpecialCharacters.concat([" "]); /** * Replace all different newlines characters by \n. */ function replaceNewLines(text) { if (!text) return ""; return text.replace(newLineRegexp, "\n"); } /** * Determine if the numbers are consecutive. */ function isConsecutive(iterable) { const array = Array.from(iterable).sort((a, b) => a - b); for (let i = 1; i < array.length; i++) if (array[i] - array[i - 1] !== 1) return false; return true; } /** * Creates a version of the function that's memoized on the value of its first argument, if any. */ function memoize(func) { const cache = /* @__PURE__ */ new Map(); const funcName = func.name ? func.name + " (memoized)" : "memoized"; return { [funcName](...args) { if (!cache.has(args[0])) cache.set(args[0], func(...args)); return cache.get(args[0]); } }[funcName]; } /** * Removes the specified indexes from the array. * Sparse (empty) elements are transformed to undefined (unless their index is explicitly removed). */ function removeIndexesFromArray(array, indexes) { const toRemove = new Set(indexes); const newArray = []; for (let i = 0; i < array.length; i++) if (!toRemove.has(i)) newArray.push(array[i]); return newArray; } function insertItemsAtIndex(array, items, index) { return array.slice(0, index).concat(items).concat(array.slice(index)); } function replaceItemAtIndex(array, newItem, index) { const newArray = [...array]; newArray[index] = newItem; return newArray; } function trimContent(content) { return content.split("\n").map((line) => line.replace(/\s+/g, " ").trim()).join("\n"); } function isNumberBetween(value, min, max) { if (min > max) return isNumberBetween(value, max, min); return value >= min && value <= max; } /** * Get a Regex for the find & replace that matches the given search string and options. */ function getSearchRegex(searchStr, searchOptions) { let searchValue = escapeRegExp(searchStr); const flags = !searchOptions.matchCase ? "i" : ""; if (searchOptions.exactMatch) searchValue = `^${searchValue}$`; return RegExp(searchValue, flags); } /** * Alternative to Math.max that works with large arrays. * Typically useful for arrays bigger than 100k elements. */ function largeMax(array) { let len = array.length; if (len < 1e5) return Math.max(...array); let max = -Infinity; while (len--) max = array[len] > max ? array[len] : max; return max; } /** * Alternative to Math.min that works with large arrays. * Typically useful for arrays bigger than 100k elements. */ function largeMin(array) { let len = array.length; if (len < 1e5) return Math.min(...array); let min = Infinity; while (len--) min = array[len] < min ? array[len] : min; return min; } var TokenizingChars = class { text; currentIndex = 0; current; constructor(text) { this.text = text; this.current = text[0]; } shift() { const current = this.current; const next = this.text[++this.currentIndex]; this.current = next; return current; } advanceBy(length) { this.currentIndex += length; this.current = this.text[this.currentIndex]; } isOver() { return this.currentIndex >= this.text.length; } remaining() { return this.text.substring(this.currentIndex); } currentStartsWith(str) { if (this.current !== str[0]) return false; for (let j = 1; j < str.length; j++) if (this.text[this.currentIndex + j] !== str[j]) return false; return true; } }; /** * Remove duplicates from an array. * * @param array The array to remove duplicates from. * @param cb A callback to get an element value. */ function removeDuplicates$1(array, cb = (a) => a) { const set = /* @__PURE__ */ new Set(); return array.filter((item) => { const key = cb(item); if (set.has(key)) return false; set.add(key); return true; }); } /** * Similar to transposing and array, but with POJOs instead of arrays. Useful, for example, when manipulating * a POJO grid[col][row] and you want to transpose it to grid[row][col]. * * The resulting object is created such as result[key1][key2] = pojo[key2][key1] */ function transpose2dPOJO(pojo) { const result = {}; for (const key in pojo) for (const subKey in pojo[key]) { if (!result[subKey]) result[subKey] = {}; result[subKey][key] = pojo[key][subKey]; } return result; } function getUniqueText(text, texts, options = {}) { const compute = options.compute ?? ((text, i) => `${text} (${i})`); const computeFirstOne = options.computeFirstOne ?? false; let i = options.start ?? 1; let newText = computeFirstOne ? compute(text, i) : text; while (texts.includes(newText)) newText = compute(text, i++); return newText; } function isFormula(content) { return content.startsWith("=") || content.startsWith("+"); } function chartStyleToCellStyle(style) { return { bold: style.bold, italic: style.italic, fontSize: style.fontSize, textColor: style.color, align: style.align }; } function doesCellContainFunction(cell, formula) { return cell.isFormula && cell.compiledFormula.usesSymbol(formula); } /** Return the number of cols/rows missing for result of the formula to be able to spread */ function getMissingHeadersForSpreadResult(getters, position, formula) { const { sheetId, col, row } = position; if (!isFormula(formula)) return; const evaluated = getters.evaluateFormula(sheetId, formula, { sheetId, col, row }); if (!isMatrix(evaluated)) return; const numberOfRows = getters.getNumberRows(sheetId); const numberOfCols = getters.getNumberCols(sheetId); return { missingRows: row + evaluated[0].length - numberOfRows, missingCols: col + evaluated.length - numberOfCols }; } //#endregion //#region src/helpers/coordinates.ts /** * Convert a (col) number to the corresponding letter. * * Examples: * 0 => 'A' * 25 => 'Z' * 26 => 'AA' * 27 => 'AB' */ function numberToLetters(n) { if (n < 0) throw new Error(`number must be positive. Got ${n}`); if (n < 26) return String.fromCharCode(65 + n); else return numberToLetters(Math.floor(n / 26) - 1) + numberToLetters(n % 26); } function lettersToNumber(letters) { let result = 0; const l = letters.length; for (let i = 0; i < l; i++) { const colIndex = charToNumber(letters[i]); result = result * 26 + colIndex; } return result - 1; } function charToNumber(char) { const charCode = char.charCodeAt(0); return charCode >= 65 && charCode <= 90 ? charCode - 64 : charCode - 96; } function isCharALetter(char) { return char >= "A" && char <= "Z" || char >= "a" && char <= "z"; } function isCharADigit(char) { return char >= "0" && char <= "9"; } const MAX_COL = lettersToNumber("ZZZ"); const MAX_ROW = 9999998; function consumeSpaces(chars) { while (chars.current === " ") chars.advanceBy(1); } function consumeLetters(chars) { if (chars.current === "$") chars.advanceBy(1); if (!chars.current || !isCharALetter(chars.current)) return -1; let colCoordinate = 0; while (chars.current && isCharALetter(chars.current)) colCoordinate = colCoordinate * 26 + charToNumber(chars.shift()); return colCoordinate; } function consumeDigits(chars) { if (chars.current === "$") chars.advanceBy(1); if (!chars.current || !isCharADigit(chars.current)) return -1; let num = 0; while (chars.current && isCharADigit(chars.current)) num = num * 10 + Number(chars.shift()); return num; } /** * Convert a "XC" coordinate to cartesian coordinates. * * Examples: * A1 => [0,0] * B3 => [1,2] * * Note: it also accepts lowercase coordinates, but not fixed references */ function toCartesian(xc) { const chars = new TokenizingChars(xc); consumeSpaces(chars); const letterPart = consumeLetters(chars); if (letterPart === -1 || !chars.current) throw new Error(`Invalid cell description: ${xc}`); const num = consumeDigits(chars); consumeSpaces(chars); const col = letterPart - 1; const row = num - 1; if (!chars.isOver() || col > MAX_COL || row > 9999998) throw new Error(`Invalid cell description: ${xc}`); return { col, row }; } /** * Convert from cartesian coordinate to the "XC" coordinate system. * * Examples: * - 0,0 => A1 * - 1,2 => B3 * - 0,0, {colFixed: false, rowFixed: true} => A$1 * - 1,2, {colFixed: true, rowFixed: false} => $B3 */ function toXC(col, row, rangePart = { colFixed: false, rowFixed: false }) { return (rangePart.colFixed ? "$" : "") + numberToLetters(col) + (rangePart.rowFixed ? "$" : "") + String(row + 1); } //#endregion //#region src/helpers/recompute_zones.ts /** * #################################################### * # INTRODUCTION * #################################################### * * This file contain the function recomputeZones. * This function try to recompute in a performant way * an ensemble of zones possibly overlapping to avoid * overlapping and to reduce the number of zones. * * It also allows to remove some zones from the ensemble. * * In the following example, 2 zones are overlapping. * Applying recomputeZones will return zones without * overlapping: * * ["B3:D4", "D2:E3"] ["B3:C4", "D2:D4", "E2:E3"] * * A B C D E A B C D E * 1 ___ 1 ___ * 2 ___|_ | 2 ___| | | * 3 | |_|_| ---> 3 | | |_| * 4 |_____| 4 |___|_| * 6 6 * 7 7 * * * In the following example, 2 zones are contiguous. * Applying recomputeZones will return only one zone: * * ["B2:B3", "C2:D3"] ["B2:D3"] * * A B C D E A B C D E * 1 _ ___ 1 _____ * 2 | | | ---> 2 | | * 3 |_|___| 3 |_____| * 4 4 * * * In the following example, we want to remove a zone * from the ensemble. Applying recomputeZones will * return the ensemble without the zone to remove: * * remove ["C3:D3"] ["B2:B4", "C2:D2", * "C4:D4", "E2:E4"] * * A B C D E F A B C D E F * 1 _______ 1 _______ * 2 | | ---> 2 | |___| | * 3 | xxx | 3 | |___| | * 4 |_______| 4 |_|___|_| * 5 5 * * * The exercise seems simple when we have only 2 zones. * But with n zones and in a performant way, we want to * avoid comparing each zone with all the others. * * * #################################################### * # Methodological approach * #################################################### * * The methodological approach to avoid comparing each * zone with all the others is to use a data structure * that allow to quickly find which zones are * overlapping with any other given zone. * * Here the idea is to profile the zones at the columns level. * * To do that, we propose to use a data structure * composed of 2 parts: * - profilesStartingPosition: a sorted number array * indicating on which columns a new profile begins. * - profiles: a map where the key is a column * position (from profilesStartingPosition) and the * value is a sorted number array representing a * profile. * * * See the following example: here profileStartingPosition * corresponds to [A,C,E,G,K] * A B C D E F G H I J K so with number [0,2,4,6,10] * 1 ' ' ' ' * 2 ' ' '_______' here profile correspond * 3 '___' |_______| for A to [] * 4 | | for C to [3, 5] * 5 |___| for E to [] * 6 for G to [2, 3] * 7 for K to [] * * * Now we can easily find which zones are overlapping * with a given zone. Suppose we want to add a new zone * D5:H6 to the ensemble: * * With a binary search of left and right * A B C D E F G H I J K on profilesStartingPosition, we can * 1 ' ' ' ' find the indexes of the profiles on which * 2 ' ' '_______' to apply a modification. * 3 '___' |_______| * 4 | _|_______ Here we will: * 5 |_|_| | - add a new profile in D --> become [3, 6] * 6 |_________| - modify the profile in E --> become [4, 6] * 7 - modify the profile in G --> become [2, 3, 4, 6] * - add a new profile in I --> become [8, 10] * * See below the result: * * Note the particularity of the profile * A B C D E F G H I J K for G: it will correspond to [2, 3, 4, 6] * 1 ' ' ' ' ' ' * 2 ' ' ' '___'___' To know how to modify the profile (add a * 3 '_'_' |___|___| zone or remove it) we do a binary * 4 | | |___ ___ search of the top and bottom value on the * 5 |_| | | | profile array. Depending on the result index * 6 |_|___|___| parity (odd or even), because zone boundaries * 7 go by pairs, we know if we are in a zone or * not and how operate. */ /** * Recompute the zone without the cells in toRemoveZones and avoid overlapping. * This compute is particularly useful because after this function: * - you will find coordinate of a cell only once among all the zones * - the number of zones will be reduced to the minimum */ function recomputeZones(zones, zonesToRemove = []) { if (zones.length <= 1 && zonesToRemove.length === 0) return zones; const profilesStartingPosition = [0]; const profiles = new Map([[0, []]]); modifyProfiles(profilesStartingPosition, profiles, zones, false); modifyProfiles(profilesStartingPosition, profiles, zonesToRemove, true); return constructZonesFromProfiles(profilesStartingPosition, profiles); } function modifyProfiles(profilesStartingPosition, profiles, zones, toRemove = false) { for (const zone of zones) { const leftValue = zone.left; const rightValue = zone.right === void 0 ? void 0 : zone.right + 1; const leftIndex = findIndexAndCreateProfile(profilesStartingPosition, profiles, leftValue, true, 0); const rightIndex = findIndexAndCreateProfile(profilesStartingPosition, profiles, rightValue, false, leftIndex); for (let i = leftIndex; i <= rightIndex; i++) modifyProfile(profiles.get(profilesStartingPosition[i]), zone, toRemove); removeContiguousProfiles(profilesStartingPosition, profiles, leftIndex, rightIndex); } } function profilesContainsZone(profilesStartingPosition, profiles, zone) { const leftValue = zone.left; const rightValue = zone.right; const leftIndex = binaryPredecessorSearch(profilesStartingPosition, leftValue, 0); const rightIndex = rightValue === void 0 ? profilesStartingPosition.length - 1 : binaryPredecessorSearch(profilesStartingPosition, rightValue, leftIndex); /** * The `profilesStartingPosition` array always contains at least the value `0` at its first position, * ensuring that applying `binaryPredecessorSearch` will always return a valid index. * Therefore, it is not necessary to check if the result of `binaryPredecessorSearch` equals `-1`. */ const topValue = zone.top; const bottomValue = zone.bottom === void 0 ? void 0 : zone.bottom + 1; for (let i = leftIndex; i <= rightIndex; i++) { const profile = profiles.get(profilesStartingPosition[i]); const topPredIndex = binaryPredecessorSearch(profile, topValue, 0); if (topPredIndex === -1 || topPredIndex % 2 !== 0) return false; const bottomSuccIndex = bottomValue === void 0 ? profile.length : binarySuccessorSearch(profile, bottomValue, 0); if (topPredIndex + 1 !== bottomSuccIndex) return false; } return true; } function findIndexAndCreateProfile(profilesStartingPosition, profiles, value, searchLeft, startIndex) { if (value === void 0) return profilesStartingPosition.length - 1; const predecessorIndex = binaryPredecessorSearch(profilesStartingPosition, value, startIndex); if (value !== profilesStartingPosition[predecessorIndex]) { profilesStartingPosition.splice(predecessorIndex + 1, 0, value); profiles.set(value, [...profiles.get(profilesStartingPosition[predecessorIndex])]); return searchLeft ? predecessorIndex + 1 : predecessorIndex; } return searchLeft ? predecessorIndex : predecessorIndex - 1; } /** * Suppose the following Suppose we want to add We want to have the * profile: the following zone: following profile: * * A B C D E F A B C D E F A B C D E F * 1 '___' 1 ' ' 1 '___' * 2 |___| 2 '___' 2 | | * 3 ' ' 3 | | 3 | | * 4 '___' --> 4 | | --> 4 | | * 6 | | 6 |___| 6 | | * 7 |___| 7 7 |___| * 8 8 8 * * the profile for 'C' the top zone correspond Here [2, 3, 5, 8] with [3, 7] * corresponds to: to 3 and the bottom zone would be merged into [2, 8] * ____ ____ correspond to 6 * [2, 3, 5, 8] would be the profile: The difficulty of modify profile * ____ is to know what must be deleted * Note that the 'filled [3, 7] and what must be added to the * zone' are always between existing profile. * an even index and its * next index * */ function modifyProfile(profile, zone, toRemove = false) { const topValue = zone.top; const bottomValue = zone.bottom === void 0 ? void 0 : zone.bottom + 1; const newPoints = []; const topPredIndex = binaryPredecessorSearch(profile, topValue, 0, false); if (topPredIndex % 2 !== 0 && !toRemove || topPredIndex % 2 === 0 && toRemove) newPoints.push(topValue); if (bottomValue === void 0) { profile.splice(topPredIndex + 1); profile.push(...newPoints); return; } const bottomSuccIndex = binarySuccessorSearch(profile, bottomValue, 0, false); if (bottomSuccIndex % 2 === 0 && !toRemove || bottomSuccIndex % 2 !== 0 && toRemove) newPoints.push(bottomValue); const toDelete = bottomSuccIndex - topPredIndex - 1; const toInsert = newPoints.length; const start = topPredIndex + 1; if (start === profile.length - 1 && toDelete === 1 && toInsert === 1) profile[start] = newPoints[0] ?? newPoints[1]; else profile.splice(start, toDelete, ...newPoints); } function removeContiguousProfiles(profilesStartingPosition, profiles, leftIndex, rightIndex) { const start = leftIndex - 1 === -1 ? 0 : leftIndex - 1; const end = rightIndex === profilesStartingPosition.length - 1 ? rightIndex : rightIndex + 1; for (let i = end; i > start; i--) if (deepEqualsArray(profiles.get(profilesStartingPosition[i]), profiles.get(profilesStartingPosition[i - 1]))) { profiles.delete(profilesStartingPosition[i]); profilesStartingPosition.splice(i, 1); } } function constructZonesFromProfiles(profilesStartingPosition, profiles) { const mergedZone = []; let pendingZones = []; for (let colIndex = 0; colIndex < profilesStartingPosition.length; colIndex++) { const left = profilesStartingPosition[colIndex]; const profile = profiles.get(left); if (!profile || profile.length === 0) { mergedZone.push(...pendingZones); pendingZones = []; continue; } let right = profilesStartingPosition[colIndex + 1]; if (right !== void 0) right--; const nextPendingZones = []; for (let i = 0; i < profile.length; i += 2) { const top = profile[i]; let bottom = profile[i + 1]; if (bottom !== void 0) bottom--; const profileZone = { top, left, bottom, right }; if (bottom === void 0 && top !== 0 || right === void 0 && left !== 0) profileZone.hasHeader = true; let findCorrespondingZone = false; for (let j = pendingZones.length - 1; j >= 0; j--) { const pendingZone = pendingZones[j]; if (pendingZone.top === profileZone.top && pendingZone.bottom === profileZone.bottom) { pendingZone.right = profileZone.right; pendingZones.splice(j, 1); nextPendingZones.push(pendingZone); findCorrespondingZone = true; break; } } if (!findCorrespondingZone) nextPendingZones.push(profileZone); } mergedZone.push(...pendingZones); pendingZones = nextPendingZones; } mergedZone.push(...pendingZones); return mergedZone; } function binaryPredecessorSearch(arr, val, start = 0, matchEqual = true) { let end = arr.length - 1; let result = -1; while (start <= end) { const mid = start + (end - start >> 1); if (arr[mid] === val && matchEqual) return mid; else if (arr[mid] < val) { result = mid; start = mid + 1; } else end = mid - 1; } return result; } function binarySuccessorSearch(arr, val, start = 0, matchEqual = true) { let end = arr.length - 1; let result = arr.length; while (start <= end) { const mid = start + (end - start >> 1); if (arr[mid] === val && matchEqual) return mid; else if (arr[mid] > val) { result = mid; end = mid - 1; } else start = mid + 1; } return result; } //#endregion //#region src/helpers/zones.ts /** * Convert from a cartesian reference to a Zone * The range boundaries will be kept in the same order as the * ones in the text. * Examples: * "A1" ==> Top 0, Bottom 0, Left: 0, Right: 0 * "B1:B3" ==> Top 0, Bottom 3, Left: 1, Right: 1 * "Sheet1!A1" ==> Top 0, Bottom 0, Left: 0, Right: 0 * "Sheet1!B1:B3" ==> Top 0, Bottom 3, Left: 1, Right: 1 * "C3:A1" ==> Top 2, Bottom 0, Left 2, Right 0 * "A:A" ==> Top 0, Bottom undefined, Left 0, Right 0 * "A:B3" or "B3:A" ==> Top 2, Bottom undefined, Left 0, Right 1 * * @param xc the string reference to convert * */ function toZoneWithoutBoundaryChanges(xc) { const chars = new TokenizingChars(xc); consumeSpaces(chars); const sheetSeparatorIndex = xc.indexOf("!"); if (sheetSeparatorIndex !== -1) chars.advanceBy(sheetSeparatorIndex + 1); const leftLetters = consumeLetters(chars); const leftNumbers = consumeDigits(chars); let top, bottom, left, right; let fullCol = false; let fullRow = false; let hasHeader = false; if (leftNumbers === -1) { left = right = leftLetters - 1; top = bottom = 0; fullCol = true; } else if (leftLetters === -1) { top = bottom = leftNumbers - 1; left = right = 0; fullRow = true; } else { left = right = leftLetters - 1; top = bottom = leftNumbers - 1; hasHeader = true; } consumeSpaces(chars); if (chars.current === ":") { chars.advanceBy(1); consumeSpaces(chars); const rightLetters = consumeLetters(chars); const rightNumbers = consumeDigits(chars); if (rightNumbers === -1) { right = rightLetters - 1; fullCol = true; } else if (rightLetters === -1) { bottom = rightNumbers - 1; fullRow = true; } else { right = rightLetters - 1; bottom = rightNumbers - 1; top = fullCol ? bottom : top; left = fullRow ? right : left; hasHeader = true; } } const zone = { top, left, bottom: fullCol ? void 0 : bottom, right: fullRow ? void 0 : right }; hasHeader = hasHeader && (fullRow || fullCol); if (hasHeader) zone.hasHeader = hasHeader; return zone; } /** * Convert from a cartesian reference to a (possibly unbounded) Zone * * Examples: * "A1" ==> Top 0, Bottom 0, Left: 0, Right: 0 * "B1:B3" ==> Top 0, Bottom 3, Left: 1, Right: 1 * "B:B" ==> Top 0, Bottom undefined, Left: 1, Right: 1 * "B2:B" ==> Top 1, Bottom undefined, Left: 1, Right: 1, hasHeader: 1 * "Sheet1!A1" ==> Top 0, Bottom 0, Left: 0, Right: 0 * "Sheet1!B1:B3" ==> Top 0, Bottom 3, Left: 1, Right: 1 * * @param xc the string reference to convert * */ function toUnboundedZone(xc) { const orderedZone = reorderZone(toZoneWithoutBoundaryChanges(xc)); const bottom = orderedZone.bottom; const right = orderedZone.right; if (bottom !== void 0 && bottom > 9999998 || right !== void 0 && right > MAX_COL) throw new Error(`Range string out of bounds: ${xc}`); if (bottom === void 0 && right === void 0) throw new Error("Wrong zone xc. The zone cannot be at the same time a full column and a full row"); return orderedZone; } /** * Convert from a cartesian reference to a Zone. * Will return throw an error if given a unbounded zone (eg : A:A). * * Examples: * "A1" ==> Top 0, Bottom 0, Left: 0, Right: 0 * "B1:B3" ==> Top 0, Bottom 2, Left: 1, Right: 1 * "Sheet1!A1" ==> Top 0, Bottom 0, Left: 0, Right: 0 * "Sheet1!B1:B3" ==> Top 0, Bottom 2, Left: 1, Right: 1 * * @param xc the string reference to convert * */ function toZone(xc) { const zone = toUnboundedZone(xc); if (zone.bottom === void 0 || zone.right === void 0) throw new Error("This does not support unbounded ranges"); return zone; } function isXcValid(xc) { return isZoneValid(toUnboundedZone(xc)); } /** * Check that the given string is a correct xc representation (ie a valid zone). The try-catch * added over the ixXcValid call is necessary because the function can throw an error if the * string is not convertible to a zone by the toUnboundedZone function. */ function isXcRepresentation(xc) { try { return isXcValid(xc); } catch (e) { return false; } } /** * Check that the zone has valid coordinates and in * the correct order. */ function isZoneValid(zone) { const { bottom, top, left, right } = zone; if (bottom !== void 0 && isNaN(bottom) || isNaN(top) || isNaN(left) || right !== void 0 && isNaN(right)) return false; return isZoneOrdered(zone) && zone.top >= 0 && zone.left >= 0; } /** * Check that the zone properties are in the correct order. */ function isZoneOrdered(zone) { return (zone.bottom === void 0 || zone.bottom >= zone.top && zone.bottom >= 0) && (zone.right === void 0 || zone.right >= zone.left && zone.right >= 0); } /** * Convert from zone to a cartesian reference * */ function zoneToXc(zone) { const { top, bottom, left, right } = zone; const hasHeader = "hasHeader" in zone ? zone.hasHeader : false; const isOneCell = top === bottom && left === right; if (bottom === void 0 && right !== void 0) return top === 0 && !hasHeader ? `${numberToLetters(left)}:${numberToLetters(right)}` : `${toXC(left, top)}:${numberToLetters(right)}`; else if (right === void 0 && bottom !== void 0) return left === 0 && !hasHeader ? `${top + 1}:${bottom + 1}` : `${toXC(left, top)}:${bottom + 1}`; else if (bottom !== void 0 && right !== void 0) return isOneCell ? toXC(left, top) : `${toXC(left, top)}:${toXC(right, bottom)}`; throw new Error("Bad zone format"); } /** * Expand a z