UNPKG

@odoo/o-spreadsheet

Version:
1,498 lines (1,487 loc) 2.19 MB
/** * This file is generated by o-spreadsheet build tools. Do not edit it. * @see https://github.com/odoo/o-spreadsheet * @version 19.1.0-alpha.3 * @date 2026-01-21T11:06:38.842Z * @hash ceae12a */ (function (exports) { 'use strict'; class FunctionCodeBuilder { scope; code = ""; constructor(scope = new Scope()) { this.scope = scope; } append(...lines) { this.code += lines.map((line) => line.toString()).join("\n") + "\n"; } return(expression) { return new FunctionCodeImpl(this.scope, this.code, expression); } toString() { return indentCode(this.code); } } class FunctionCodeImpl { scope; returnExpression; code; constructor(scope, code, returnExpression) { this.scope = scope; this.returnExpression = returnExpression; this.code = indentCode(code); } toString() { return this.code; } assignResultToVariable() { if (this.scope.isAlreadyDeclared(this.returnExpression)) { return this; } const variableName = this.scope.nextVariableName(); const code = new FunctionCodeBuilder(this.scope); code.append(this.code); code.append(`const ${variableName} = ${this.returnExpression};`); return code.return(variableName); } } class Scope { nextId = 1; declaredVariables = new Set(); nextVariableName() { const name = `_${this.nextId++}`; this.declaredVariables.add(name); return name; } isAlreadyDeclared(name) { return this.declaredVariables.has(name); } } /** * Takes a list of strings that might be single or multiline * and maps them in a list of single line strings. */ function splitLines(str) { return str .split("\n") .map((line) => line.trim()) .filter((line) => line !== ""); } function indentCode(code) { let result = ""; let indentLevel = 0; const lines = splitLines(code); for (const line of lines) { if (line.startsWith("}")) { indentLevel--; } result += "\t".repeat(indentLevel) + line + "\n"; if (line.endsWith("{")) { indentLevel++; } } return result.trim(); } //------------------------------------------------------------------------------ // Arg description DSL //------------------------------------------------------------------------------ const ARG_REGEXP = /(.*?)\((.*?)\)(.*)/; const ARG_TYPES = [ "ANY", "BOOLEAN", "DATE", "NUMBER", "STRING", "RANGE", "RANGE<BOOLEAN>", "RANGE<DATE>", "RANGE<NUMBER>", "RANGE<STRING>", "META", "RANGE<META>", ]; function arg(definition, description = "", proposals) { return makeArg(definition, description, proposals); } function makeArg(str, description, proposals) { const parts = str.match(ARG_REGEXP); const name = parts[1].trim(); if (!name) { throw new Error(`Function argument definition is missing a name: '${str}'.`); } const types = []; let isOptional = false; let isRepeating = false; let defaultValue; for (const param of parts[2].split(",")) { const key = param.trim().toUpperCase(); const type = ARG_TYPES.find((t) => key === t); if (type) { types.push(type); } else if (key === "RANGE<ANY>") { types.push("RANGE"); } else if (key === "OPTIONAL") { isOptional = true; } else if (key === "REPEATING") { isRepeating = true; } else if (key.startsWith("DEFAULT=")) { defaultValue = param.trim().slice(8); } } const result = { name, description, type: types, }; const acceptErrors = types.includes("ANY") || types.includes("RANGE"); if (acceptErrors) { result.acceptErrors = true; } if (isOptional) { result.optional = true; } if (isRepeating) { result.repeating = true; } if (defaultValue !== undefined) { result.default = true; result.defaultValue = defaultValue; } if (types.some((t) => t.startsWith("RANGE"))) { result.acceptMatrix = true; } if (types.every((t) => t.startsWith("RANGE"))) { result.acceptMatrixOnly = true; } if (proposals && proposals.length > 0) { result.proposalValues = proposals; } return result; } /** * This function adds on description more general information derived from the * arguments. * * This information is useful during compilation. */ function addMetaInfoFromArg(name, addDescr) { let countArg = 0; let minArg = 0; let repeatingArg = 0; let optionalArg = 0; for (const arg of addDescr.args) { countArg++; if (!arg.optional && !arg.default) { minArg++; } if (arg.repeating) { repeatingArg++; } if ((arg.optional || arg.default) && !arg.repeating) { optionalArg++; } } const descr = addDescr; descr.minArgRequired = minArg; descr.maxArgPossible = repeatingArg ? Infinity : countArg; descr.nbrArgRepeating = repeatingArg; descr.nbrOptionalNonRepeatingArgs = optionalArg; descr.hidden = addDescr.hidden || false; descr.name = name; return descr; } const cacheArgTargeting = {}; /** * Returns a function that maps the position of a value in a function to its corresponding argument index. * * In most cases, the task is straightforward: * * In the formula "=SUM(11, 55, 66)" which is defined like this "SUM(value1, [value2, ...])": * - 11 corresponds to the value1 argument => position will be 0 * - 55 and 66 correspond to the [value2, ...] argument => position will be 1 * * In other cases, optional arguments could be defined after repeatable arguments, * or even optional and required arguments could be mixed in unconventional ways. * * The next function has been designed to handle all possible configurations. * The only restriction is if repeatable arguments are present in the function definition: * - they must be defined consecutively * - they must be in a quantity greater than the optional arguments. * * The markdown tables below illustrate how values are mapped to positions based on the number of values supplied. * Each table represents a different function configuration, with columns representing the number of values supplied as arguments * and rows representing the correspondence with the argument index. * * The tables are built based on the following conventions: * - `m`: Mandatory argument (count as one argument) * - `o`: Optional argument (count as zero or one argument) * - `r`: Repeating argument (count as one or more arguments) * * * Configuration 1: (m, o) like the CEILING function * * | | 1 | 2 | * |---|---|---| * | m | 0 | 0 | * | o | | 1 | * * * Configuration 2: (m, m, m, r, r) like the SUMIFS function * * | | 5 | 7 | 3 + 2n | * |---|---|------|------------| * | m | 0 | 0 | 0 | * | m | 1 | 1 | 1 | * | m | 2 | 2 | 2 | * | r | 3 | 3, 5 | 3 + 2n | * | r | 4 | 4, 6 | 3 + 2n + 1 | * * * Configuration 3: (m, m, m, r, r, o) like the SWITCH function * * | | 5 | 6 | 7 | 8 | 3 + 2n | 3 + 2n + 1 | * |---|---|---|------|------|------------|----------------| * | m | 0 | 0 | 0 | 0 | 0 | 0 | * | m | 1 | 1 | 1 | 1 | 1 | 1 | * | m | 2 | 2 | 2 | 2 | 2 | 2 | * | r | 3 | 3 | 3, 5 | 3, 5 | 3 + 2n | 3 + 2n | * | r | 4 | 4 | 4, 6 | 4, 6 | 3 + 2n + 1 | 3 + 2n + 1 | * | o | | 5 | | 7 | | 3 + 2N + 2 | * * * Configuration 4: (m, o, m, o, r, r, r, m) a complex case to understand subtleties * * | | 6 | 7 | 8 | 9 | 10 | 11 | ... | * |---|---|---|---|------|------|------|-----| * | m | 0 | 0 | 0 | 0 | 0 | 0 | ... | * | o | | 1 | 1 | | 1 | 1 | ... | * | m | 1 | 2 | 2 | 1 | 2 | 2 | ... | * | o | | | 3 | | | 3 | ... | * | r | 2 | 3 | 4 | 2, 5 | 3, 6 | 4, 7 | ... | * | r | 3 | 4 | 5 | 3, 6 | 4, 7 | 5, 8 | ... | * | r | 4 | 5 | 6 | 4, 7 | 5, 8 | 6, 9 | ... | * | m | 5 | 6 | 7 | 8 | 9 | 10 | ... | * */ function argTargeting(functionDescription, nbrArgSupplied) { const functionName = functionDescription.name; const result = cacheArgTargeting[functionName]?.[nbrArgSupplied]; if (result) { return result; } if (!cacheArgTargeting[functionName]) { cacheArgTargeting[functionName] = {}; } if (!cacheArgTargeting[functionName][nbrArgSupplied]) { cacheArgTargeting[functionName][nbrArgSupplied] = _argTargeting(functionDescription, nbrArgSupplied); } return cacheArgTargeting[functionName][nbrArgSupplied]; } function _argTargeting(functionDescription, nbrArgSupplied) { const valueIndexToArgPosition = {}; const groupsOfOptionalRepeatingValues = functionDescription.nbrArgRepeating ? Math.floor((nbrArgSupplied - functionDescription.minArgRequired) / functionDescription.nbrArgRepeating) : 0; const nbrValueOptionalRepeating = functionDescription.nbrArgRepeating * groupsOfOptionalRepeatingValues; const nbrValueOptional = nbrArgSupplied - functionDescription.minArgRequired - nbrValueOptionalRepeating; let countValueSupplied = 0; let countValueOptional = 0; for (let i = 0; i < functionDescription.args.length; i++) { const arg = functionDescription.args[i]; if ((arg.optional || arg.default) && !arg.repeating) { if (countValueOptional < nbrValueOptional) { valueIndexToArgPosition[countValueSupplied] = { index: i }; countValueSupplied++; } countValueOptional++; continue; } if (arg.repeating) { const groupOfMandatoryRepeatingValues = arg.optional ? 0 : 1; // As we know all repeating arguments are consecutive, // --> we will treat all repeating arguments in one go // --> the index i will be incremented by the number of repeating values at the end of the loop for (let j = 0; j < groupsOfOptionalRepeatingValues + groupOfMandatoryRepeatingValues; j++) { for (let k = 0; k < functionDescription.nbrArgRepeating; k++) { valueIndexToArgPosition[countValueSupplied] = { index: i + k, repeatingGroupIndex: j }; countValueSupplied++; } } i += functionDescription.nbrArgRepeating - 1; continue; } // End case: it's a required argument valueIndexToArgPosition[countValueSupplied] = { index: i }; countValueSupplied++; } return (argPosition) => { return valueIndexToArgPosition[argPosition]; }; } //------------------------------------------------------------------------------ // Argument validation //------------------------------------------------------------------------------ const META_TYPES = ["META", "RANGE<META>"]; function validateArguments(descr) { if (descr.nbrArgRepeating && descr.nbrOptionalNonRepeatingArgs >= descr.nbrArgRepeating) { throw new Error(`Function ${descr.name} has more optional arguments than repeatable ones.`); } let foundRepeating = false; let consecutiveRepeating = false; for (const current of descr.args) { if (current.type.some((t) => META_TYPES.includes(t)) && current.type.some((t) => !META_TYPES.includes(t))) { throw new Error(`Function ${descr.name} has a mix of META and non-META types in the same argument: ${current.type}.`); } if (current.repeating) { if (!consecutiveRepeating && foundRepeating) { throw new Error(`Function ${descr.name} has non-consecutive repeating arguments. All repeating arguments must be declared consecutively.`); } foundRepeating = true; consecutiveRepeating = true; } else { consecutiveRepeating = false; } } } let Registry$1 = class Registry { content = {}; add(key, value) { if (key in this.content) { throw new Error(`${key} is already present in this registry!`); } return this.replace(key, value); } replace(key, value) { this.content[key] = value; return this; } get(key) { const content = this.content[key]; if (!content && !(key in this.content)) { throw new Error(`Cannot find ${key} in this registry!`); } return content; } contains(key) { return key in this.content; } getAll() { return Object.values(this.content); } getKeys() { return Object.keys(this.content); } remove(key) { delete this.content[key]; } }; const defaultTranslate = (s) => s; const defaultLoaded = () => false; let _translate = defaultTranslate; let _loaded = defaultLoaded; function sprintf(s, ...values) { if (values.length === 1 && typeof values[0] === "object" && !(values[0] instanceof String)) { const valuesDict = values[0]; s = s.replace(/\%\(([^\)]+)\)s/g, (match, value) => valuesDict[value]); } else if (values.length > 0) { s = s.replace(/\%s/g, () => values.shift()); } return s; } /*** * Allow to inject a translation function from outside o-spreadsheet. This should be called before instantiating * a model. * @param tfn the function that will do the translation * @param loaded a function that returns true when the translation is loaded */ function setTranslationMethod(tfn, loaded = () => true) { _translate = tfn; _loaded = loaded; } /** * If no translation function has been set, this will mark the translation are loaded. * * By default, the translations should not be set as loaded, otherwise top-level translated constants will never be * translated. But if by the time the model is instantiated no custom translation function has been set, we can set * the default translation function as loaded so o-spreadsheet can be run in standalone with no translations. */ function setDefaultTranslationMethod() { if (_translate === defaultTranslate && _loaded === defaultLoaded) { _loaded = () => true; } } const _t = function (s, ...values) { if (!_loaded()) { return new LazyTranslatedString(s, values); } return sprintf(_translate(s), ...values); }; class LazyTranslatedString extends String { values; constructor(str, values) { super(str); this.values = values; } valueOf() { const str = super.valueOf(); return _loaded() ? sprintf(_translate(str), ...this.values) : sprintf(str, ...this.values); } toString() { return this.valueOf(); } } const CellErrorType = { NotAvailable: "#N/A", InvalidReference: "#REF", BadExpression: "#BAD_EXPR", CircularDependency: "#CYCLE", UnknownFunction: "#NAME?", DivisionByZero: "#DIV/0!", InvalidNumber: "#NUM!", SpilledBlocked: "#SPILL!", GenericError: "#ERROR", NullError: "#NULL!", }; const errorTypes = new Set(Object.values(CellErrorType)); class EvaluationError { message; value; constructor(message = _t("Error"), value = CellErrorType.GenericError) { this.message = message; this.value = value; this.message = message.toString(); } } class BadExpressionError extends EvaluationError { constructor(message = _t("Invalid expression")) { super(message, CellErrorType.BadExpression); } } class CircularDependencyError extends EvaluationError { constructor(message = _t("Circular reference")) { super(message, CellErrorType.CircularDependency); } } class InvalidReferenceError extends EvaluationError { constructor(message = _t("Invalid reference")) { super(message, CellErrorType.InvalidReference); } } class NotAvailableError extends EvaluationError { constructor(message = _t("Data not available")) { super(message, CellErrorType.NotAvailable); } } class UnknownFunctionError extends EvaluationError { constructor(message = _t("Unknown function")) { super(message, CellErrorType.UnknownFunction); } } class SplillBlockedError extends EvaluationError { constructor(message = _t("Spill range is not empty")) { super(message, CellErrorType.SpilledBlocked); } } class DivisionByZeroError extends EvaluationError { constructor(message = _t("Division by zero")) { super(message, CellErrorType.DivisionByZero); } } class NumberTooLargeError extends EvaluationError { constructor(message = _t("Number too large")) { super(message, CellErrorType.InvalidNumber); } } const borderStyles = ["thin", "medium", "thick", "dashed", "dotted"]; function isMatrix(x) { return Array.isArray(x) && Array.isArray(x[0]); } exports.DIRECTION = void 0; (function (DIRECTION) { DIRECTION["UP"] = "up"; DIRECTION["DOWN"] = "down"; DIRECTION["LEFT"] = "left"; DIRECTION["RIGHT"] = "right"; })(exports.DIRECTION || (exports.DIRECTION = {})); // Colors const HIGHLIGHT_COLOR = "#017E84"; const SELECTION_BORDER_COLOR = "#3266ca"; const BACKGROUND_CHART_COLOR = "#FFFFFF"; const LINK_COLOR = HIGHLIGHT_COLOR; const GRAY_900 = "#111827"; 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_PADDING = 20; const CHART_PADDING_BOTTOM = 10; const CHART_PADDING_TOP = 15; const CHART_TITLE_FONT_SIZE = 16; const CHART_AXIS_TITLE_FONT_SIZE = 12; const SCORECARD_CHART_TITLE_FONT_SIZE = 14; // Color picker defaults as upper case HEX to match `toHex`helper 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_WIDTH = 96; const DEFAULT_CELL_HEIGHT = 23; const SCROLLBAR_WIDTH = 15; const MIN_CF_ICON_MARGIN = 4; const MIN_CELL_TEXT_MARGIN = 4; const PADDING_AUTORESIZE_VERTICAL = 3; const PADDING_AUTORESIZE_HORIZONTAL = MIN_CELL_TEXT_MARGIN; const GRID_ICON_MARGIN = 2; const GRID_ICON_EDGE_LENGTH = 17; const FOOTER_HEIGHT = 2 * DEFAULT_CELL_HEIGHT; const DATA_VALIDATION_CHIP_MARGIN = 5; // Style const DEFAULT_STYLE = { align: "left", verticalAlign: "bottom", wrapping: "overflow", bold: false, italic: false, strikethrough: false, underline: false, fontSize: 10, fillColor: "", textColor: "", rotation: 0, }; const DEFAULT_VERTICAL_ALIGN = DEFAULT_STYLE.verticalAlign; // Fonts const DEFAULT_FONT_WEIGHT = "400"; const DEFAULT_FONT_SIZE = DEFAULT_STYLE.fontSize; const DEFAULT_FONT = "'Roboto', arial"; // Max Number of history steps kept in memory const MAX_HISTORY_STEPS = 99; // Id of the first revision const DEFAULT_REVISION_ID = "START_REVISION"; // Figure const DEFAULT_FIGURE_HEIGHT = 335; const DEFAULT_FIGURE_WIDTH = 536; const FIGURE_BORDER_WIDTH = 1; // Chart const MAX_CHAR_LABEL = 20; const FIGURE_ID_SPLITTER = "??"; const DEFAULT_SCORECARD_KEY_VALUE_FONT_SIZE = 32; const DEFAULT_SCORECARD_BASELINE_FONT_SIZE = 16; const DEFAULT_WINDOW_SIZE = 2; // session const DEBOUNCE_TIME = 200; const MESSAGE_VERSION = 1; const FORBIDDEN_SHEETNAME_CHARS_IN_EXCEL_REGEX = /'|\*|\?|\/|\\|\[|\]/; // Cells const FORMULA_REF_IDENTIFIER = "|"; let DEFAULT_SHEETVIEW_SIZE = 0; function getDefaultSheetViewSize() { return DEFAULT_SHEETVIEW_SIZE; } const NEWLINE = "\n"; // Pivot const PIVOT_TABLE_CONFIG = { hasFilters: false, totalRow: false, firstColumn: true, lastColumn: false, numberOfHeaders: 1, bandedRows: true, bandedColumns: false, styleId: "TableStyleMedium5", automaticAutofill: false, }; const PIVOT_INDENT = 15; const PIVOT_COLLAPSE_ICON_SIZE = 12; const PIVOT_MAX_NUMBER_OF_CELLS = 5e5; //------------------------------------------------------------------------------ // Miscellaneous //------------------------------------------------------------------------------ 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 can be undefined when there's no prototype (`Object.create(null, {})`) (obj?.constructor === Object || obj?.constructor === undefined)); } /** * 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) { const lastGroup = groups[groups.length - 1]; lastGroup.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 ? undefined : nextItem.value, }; } } function isBoolean(str) { const upperCased = str.toUpperCase(); return upperCased === "TRUE" || upperCased === "FALSE"; } const MARKDOWN_LINK_REGEX = /^\[(.+)\]\((.+)\)$/; //link must start with http or https //https://stackoverflow.com/a/3809435/4760614 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(O_SPREADSHEET_LINK_PREFIX.length); } 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 !== undefined; } function isNotNull(argument) { return argument !== null; } /** * 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 === undefined) 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 = undefined; 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 = undefined; firstCalled = false; func.apply(context, args); } clearTimeout(timeout); timeout = setTimeout(later, wait); }; debounced.isDebouncePending = () => timeout !== undefined; 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); } }; } /* * Concatenate an array of strings. */ function concat(chars) { // ~40% faster than chars.join("") 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; // Objects can have different keys if the values are undefined for (const key in o2) { if (!(key in o1) && o2[key] !== undefined) { 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 = [ "\t", "\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, NEWLINE); } /** * Determine if the numbers are consecutive. */ function isConsecutive(iterable) { const array = Array.from(iterable).sort((a, b) => a - b); // sort numerically rather than lexicographically 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 = 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) { const contentLines = content.split("\n"); return contentLines.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 < 100_000) 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 < 100_000) return Math.min(...array); let min = +Infinity; while (len--) { min = array[len] < min ? array[len] : min; } return min; } class TokenizingChars { 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(array, cb = (a) => a) { const set = 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("+"); } // TODO: we should make make ChartStyle be the same as Style sometime ... function chartStyleToCellStyle(style) { return { bold: style.bold, italic: style.italic, fontSize: style.fontSize, textColor: style.color, align: style.align, }; } // ----------------------------------------------------------------------------- // Date Type // ----------------------------------------------------------------------------- /** * A DateTime object that can be used to manipulate spreadsheet dates. * Conceptually, a spreadsheet date is simply a number with a date format, * and it is timezone-agnostic. * This DateTime object consistently uses UTC time to represent a naive date and time. */ class DateTime { jsDate; constructor(year, month, day, hours = 0, minutes = 0, seconds = 0) { this.jsDate = new Date(Date.UTC(year, month, day, hours, minutes, seconds, 0)); } static fromTimestamp(timestamp) { const date = new Date(timestamp); return new DateTime(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate(), date.getUTCHours(), date.getUTCMinutes(), date.getUTCSeconds()); } static now() { const now = new Date(); return new DateTime(now.getFullYear(), now.getMonth(), now.getDate(), now.getHours(), now.getMinutes(), now.getSeconds()); } toString() { return this.jsDate.toString(); } toLocaleDateString() { return this.jsDate.toLocaleDateString(); } getTime() { return this.jsDate.getTime(); } getFullYear() { return this.jsDate.getUTCFullYear(); } getMonth() { return this.jsDate.getUTCMonth(); } getQuarter() { return Math.floor(this.getMonth() / 3) + 1; } getDate() { return this.jsDate.getUTCDate(); } getDay() { return this.jsDate.getUTCDay(); } getHours() { return this.jsDate.getUTCHours(); } getMinutes() { return this.jsDate.getUTCMinutes(); } getSeconds() { return this.jsDate.getUTCSeconds(); } getIsoWeek() { const date = new Date(this.jsDate.getTime()); const dayNumber = date.getUTCDay() || 7; date.setUTCDate(date.getUTCDate() + 4 - dayNumber); const yearStart = new Date(Date.UTC(date.getUTCFullYear(), 0, 1)); return Math.ceil(((date.getTime() - yearStart.getTime()) / 86400000 + 1) / 7); } setFullYear(year) { return this.jsDate.setUTCFullYear(year); } setMonth(month) { return this.jsDate.setUTCMonth(month); } setDate(date) { return this.jsDate.setUTCDate(date); } setHours(hours) { return this.jsDate.setUTCHours(hours); } setMinutes(minutes) { return this.jsDate.setUTCMinutes(minutes); } setSeconds(seconds) { return this.jsDate.setUTCSeconds(seconds); } } // ----------------------------------------------------------------------------- // Parsing // ----------------------------------------------------------------------------- const INITIAL_1900_DAY = new DateTime(1899, 11, 30); const MS_PER_DAY = 24 * 60 * 60 * 1000; const CURRENT_MILLENIAL = 2000; // note: don't forget to update this in 2999 const CURRENT_YEAR = DateTime.now().getFullYear(); const CURRENT_MONTH = DateTime.now().getMonth(); const INITIAL_JS_DAY = DateTime.fromTimestamp(0); const DATE_JS_1900_OFFSET = INITIAL_JS_DAY.getTime() - INITIAL_1900_DAY.getTime(); const mdyDateRegexp = /^\d{1,2}(\/|-|\s)\d{1,2}((\/|-|\s)\d{1,4})?$/; const ymdDateRegexp = /^\d{3,4}(\/|-|\s)\d{1,2}(\/|-|\s)\d{1,2}$/; const whiteSpaceChars = whiteSpaceCharacters.join(""); const dateSeparatorsRegex = new RegExp(`\/|-|${whiteSpaceCharacters.join("|")}`); const dateRegexp = new RegExp(`^(\\d{1,4})[\/${whiteSpaceChars}\-](\\d{1,4})([\/${whiteSpaceChars}\-](\\d{1,4}))?$`); const timeRegexp = /((\d+(:\d+)?(:\d+)?\s*(AM|PM))|(\d+:\d+(:\d+)?))$/; /** Convert a value number representing a date, or return undefined if it isn't possible */ function valueToDateNumber(value, locale) { switch (typeof value) { case "number": return value; case "string": if (isDateTime(value, locale)) { return parseDateTime(value, locale)?.value; } return !value || isNaN(Number(value)) ? undefined : Number(value); default: return undefined; } } function isDateTime(str, locale) { return parseDateTime(str, locale) !== null; } const CACHE = new Map(); function parseDateTime(str, locale) { if (!CACHE.has(locale)) { CACHE.set(locale, new Map()); } if (!CACHE.get(locale).has(str)) { CACHE.get(locale).set(str, _parseDateTime(str, locale)); } return CACHE.get(locale).get(str); } function _parseDateTime(str, locale) { str = str.trim(); let time = null; const timeMatch = str.match(timeRegexp); if (timeMatch) { time = parseTime(timeMatch[0]); if (time === null) { return null; } str = str.replace(timeMatch[0], "").trim(); } let date = null; const dateParts = getDateParts(str, locale); if (dateParts) { const separator = dateParts.dateString.match(dateSeparatorsRegex)[0]; date = parseDate(dateParts, separator); if (date === null) { return null; } str = str.replace(dateParts.dateString, "").trim(); } if (str !== "" || !(date || time)) { return null; } if (date && date.jsDate && time && time.jsDate) { return { value: date.value + time.value, format: date.format + " " + (time.format === "hhhh:mm:ss" ? "hh:mm:ss" : time.format), jsDate: new DateTime(date.jsDate.getFullYear() + time.jsDate.getFullYear() - 1899, date.jsDate.getMonth() + time.jsDate.getMonth() - 11, date.jsDate.getDate() + time.jsDate.getDate() - 30, date.jsDate.getHours() + time.jsDate.getHours(), date.jsDate.getMinutes() + time.jsDate.getMinutes(), date.jsDate.getSeconds() + time.jsDate.getSeconds()), }; } return date || time; } /** * Returns the parts (day/month/year) of a date string corresponding to the given locale. * * - A string "xxxx-xx-xx" will be parsed as "y-m-d" no matter the locale. * - A string "xx-xx-xxxx" will be parsed as "m-d-y" for mdy locale, and "d-m-y" for ymd and dmy locales. * - A string "xx-xx-xx" will be "y-m-d" for ymd locale, "d-m-y" for dmy locale, "m-d-y" for mdy locale. * - A string "xxxx-xx" will be parsed as "y-m" no matter the locale. * - A string "xx-xx" will be parsed as "m-d" for mdy and ymd locales, and "d-m" for dmy locale. */ function getDateParts(dateStri