UNPKG

@bitrix24/b24jssdk

Version:

Bitrix24 REST API JavaScript SDK

450 lines (446 loc) 14 kB
/** * @package @bitrix24/b24jssdk * @version 2.2.0 * @copyright (c) 2026 Bitrix24 * @license MIT * @see https://github.com/bitrix24/b24jssdk * @see https://bitrix24.github.io/b24jssdk/ */ 'use strict'; const luxon = require('luxon'); const uuidv7 = require('./uuidv7.cjs'); const type = require('./type.cjs'); var __defProp = Object.defineProperty; var __name = (target, value) => __defProp(target, "name", { value, configurable: true }); const reEscape = /[&<>'"]/g; const reUnescape = /&(?:amp|#38|lt|#60|gt|#62|apos|#39|quot|#34)/g; const escapeEntities = { "&": "&amp", "<": "&lt", ">": "&gt", "'": "&#39", '"': "&quot" }; const unescapeEntities = { "&amp": "&", "&#38": "&", "&lt": "<", "&#60": "<", "&gt": ">", "&#62": ">", "&apos": "'", "&#39": "'", "&quot": '"', "&#34": '"' }; class TextManager { static { __name(this, "TextManager"); } /** * Generates a random `[a-z0-9]` string of the requested length. * * Each character is drawn from `Math.random()`, so the result is **not** * cryptographically secure — use it for cache-busting keys and disposable * ids, not for tokens or secrets. * * @param length - Number of characters to generate. Defaults to `8`. * @returns A random lowercase alphanumeric string. * * @example * ```ts * Text.getRandom() // 'a7f3k1z9' * Text.getRandom(4) // 'p2x8' * ``` */ getRandom(length = 8) { return Array.from({ length }).map(() => Math.trunc(Math.random() * 36).toString(36)).join(""); } /** * Generates a locally-computed UUID v4 (random) string. * * The value is built from `Math.random()` and is **not** * cryptographically secure. For a time-ordered, RFC 4122 identifier prefer * {@link getUuidRfc4122}. * * @returns A UUID v4 formatted string (`xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx`). * * @example * ```ts * Text.getUniqId() // 'd2b8a1f0-3c4e-4a9b-8f7c-1e2d3a4b5c6d' * ``` */ getUniqId() { return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => { const r = Math.trunc(Math.random() * 16); const v = c === "x" ? r : r & 3 | 8; return v.toString(16); }); } /** * Generates a time-ordered UUID v7 (RFC 4122). * * This is the identifier the SDK uses as the default request id, because its * leading timestamp keeps generated ids sortable by creation time. * * @returns A UUID v7 formatted string. * * @example * ```ts * Text.getUuidRfc4122() // '019323ac-8ace-725b-a3dc-6a7c333da066' * ``` */ getUuidRfc4122() { return uuidv7.default(); } /** * Encodes the unsafe HTML characters `&`, `<`, `>`, `'`, and `"` into their * entity codes. * * To match the legacy Bitrix Framework behaviour the trailing `;` is * deliberately omitted (`&amp` instead of `&amp;`). Non-string values are * returned untouched. * * This is **not** a general-purpose HTML sanitizer: it only escapes those * five characters and is not context-aware (it does not neutralise * attribute-breakout, `javascript:` URLs, or markup outside the escaped set). * Do not rely on it as the sole XSS defence for untrusted input rendered as * HTML. * * @param value - The string to encode. * @returns The encoded string, or the original value when it is not a string. * * @example * ```ts * Text.encode('<b>Tom & Jerry</b>') // '&ltb&gtTom &amp Jerry&lt/b&gt' * ``` */ encode(value) { if (type.Type.isString(value)) { return value.replace(reEscape, (item) => escapeEntities[item]); } return value; } /** * Decodes HTML entities produced by {@link encode} back into their * characters. * * Both the named entities (`&amp`, `&lt`, …) and their numeric equivalents * (`&#38`, `&#60`, …) are recognised. Like {@link encode}, the tokens carry * no trailing `;`, so a `;` that follows an entity in the input is left in * place (`&amp;` decodes to `&;`). Non-string values are returned untouched. * * @param value - The string to decode. * @returns The decoded string, or the original value when it is not a string. * * @example * ```ts * Text.decode('&ltb&gtTom &amp Jerry&lt/b&gt') // '<b>Tom & Jerry</b>' * ``` */ decode(value) { if (type.Type.isString(value)) { return value.replace(reUnescape, (item) => unescapeEntities[item]); } return value; } /** * Parses a value into a floating-point number. * * Uses `Number.parseFloat`, so a leading numeric portion is accepted * (`'12px'` → `12`). Any value that cannot be parsed becomes `0`. * * @param value - The value to convert. * @returns The parsed number, or `0` when parsing fails. * * @example * ```ts * Text.toNumber('12.5') // 12.5 * Text.toNumber('abc') // 0 * ``` */ toNumber(value) { const parsedValue = Number.parseFloat(value); if (type.Type.isNumber(parsedValue)) { return parsedValue; } return 0; } /** * Parses a value into an integer (base 10). * * Any value that cannot be parsed becomes `0`. * * @param value - The value to convert. * @returns The parsed integer, or `0` when parsing fails. * * @example * ```ts * Text.toInteger('42.9') // 42 * Text.toInteger('abc') // 0 * ``` */ toInteger(value) { return this.toNumber(Number.parseInt(value, 10)); } /** * Interprets a value as a boolean. * * `true` is returned for `true`, `1`, `'true'`, `'y'`, and `'1'` * (string comparison is case-insensitive). Extra truthy tokens can be added * through `trueValues`; everything else yields `false`. * * @param value - The value to interpret. * @param trueValues - Additional values that should be treated as `true`. * @returns `true` when the value matches a truthy token, otherwise `false`. * * @example * ```ts * Text.toBoolean('Y') // true * Text.toBoolean('on', ['on']) // true * Text.toBoolean('no') // false * ``` */ toBoolean(value, trueValues = []) { const transformedValue = type.Type.isString(value) ? value.toLowerCase() : value; return ["true", "y", "1", 1, true, ...trueValues].includes(transformedValue); } /** * Converts a string to `camelCase`. * * Hyphens, underscores, and whitespace are treated as word separators. A * fully uppercase string is lowercased (`'ABC'` → `'abc'`); an empty or * non-filled string is returned untouched. * * @param str - The string to convert. * @returns The `camelCase` string. * * @example * ```ts * Text.toCamelCase('get_user_id') // 'getUserId' * Text.toCamelCase('Some Value') // 'someValue' * ``` */ toCamelCase(str) { if (!type.Type.isStringFilled(str)) { return str; } const separators = /[-_\s]+(.)?/g; if (!separators.test(str)) { return /^[A-Z]+$/.test(str) ? str.toLowerCase() : str[0].toLowerCase() + str.slice(1); } const camel = str.toLowerCase().replace( separators, (_match, letter) => letter ? letter.toUpperCase() : "" ); return camel[0].toLowerCase() + camel.slice(1); } /** * Converts a string to `PascalCase`. * * Equivalent to `capitalize(toCamelCase(str))`. An empty or non-filled string * is returned untouched. * * @param str - The string to convert. * @returns The `PascalCase` string. * * @example * ```ts * Text.toPascalCase('get_user_id') // 'GetUserId' * ``` */ toPascalCase(str) { if (!type.Type.isStringFilled(str)) { return str; } return this.capitalize(this.toCamelCase(str)); } /** * Converts a string to `kebab-case`. * * Splits on uppercase-letter boundaries as well as existing separators, so * both `camelCase` and mixed-case acronyms are handled. An uppercase run that * is immediately followed by a digit is split into single letters * (`parseHTML5` → `parse-h-t-m-l-5`), because there is no word boundary * between the acronym and the digit. An empty or non-filled string is * returned untouched. * * @param str - The string to convert. * @returns The `kebab-case` string. * * @example * ```ts * Text.toKebabCase('getUserId') // 'get-user-id' * Text.toKebabCase('XMLHttpRequest') // 'xml-http-request' * ``` */ toKebabCase(str) { if (!type.Type.isStringFilled(str)) { return str; } const matches = str.match( /[A-Z]{2,}(?=[A-Z][a-z]+\d*|\b)|[A-Z]?[a-z]+\d*|[A-Z]|\d+/g ); if (!matches) { return str; } return matches.map((word) => word.toLowerCase()).join("-"); } /** * Uppercases the first character of a string, leaving the rest untouched. * * An empty or non-filled string is returned untouched. * * @param str - The string to capitalize. * @returns The capitalized string. * * @example * ```ts * Text.capitalize('hello') // 'Hello' * ``` */ capitalize(str) { if (!type.Type.isStringFilled(str)) { return str; } return str[0].toUpperCase() + str.slice(1); } /** * Formats a number with grouped thousands and a fixed number of decimals. * * Mirrors the algorithm Bitrix24 uses on the server: non-finite inputs are * treated as `0`, the fractional part is rounded to `decimals` places, and * the thousands separator is inserted every three digits left of the decimal * point. * * @param number - The number to format. * @param decimals - Number of digits after the decimal point. Defaults to `0`. * @param decPoint - The decimal-point character. Defaults to `'.'`. * @param thousandsSep - The thousands separator. Defaults to `','`. * @returns The formatted number as a string. * * @example * ```ts * Text.numberFormat(1234.567, 2) // '1,234.57' * Text.numberFormat(1234.567, 2, ',', ' ') // '1 234,57' * ``` */ numberFormat(number, decimals = 0, decPoint = ".", thousandsSep = ",") { const value = Number.isFinite(number) ? number : 0; const fractionDigits = Number.isFinite(decimals) ? Math.abs(decimals) : 0; const roundTo = /* @__PURE__ */ __name((n, digits) => { const factor = 10 ** digits; return Math.round(n * factor) / factor; }, "roundTo"); const parts = (fractionDigits ? roundTo(value, fractionDigits) : Math.round(value)).toString().split("."); if (parts[0] && parts[0].length > 3) { parts[0] = parts[0].replace(/\B(?=(?:\d{3})+(?!\d))/g, thousandsSep); } if ((parts[1] || "").length < fractionDigits) { parts[1] = (parts[1] || "").padEnd(fractionDigits, "0"); } return parts.join(decPoint); } /** * Converts a string into a Luxon `DateTime`. * * When `template` is provided the string is parsed with * `DateTime.fromFormat`; otherwise it is parsed as ISO 8601 via * `DateTime.fromISO`. * * @param dateString - The date string to parse. * @param template - Optional Luxon format token describing `dateString`. * @param opts - Optional Luxon parsing options (zone, locale, …). * @returns The parsed `DateTime` (use `.isValid` to check the result). * * @see https://moment.github.io/luxon/#/parsing?id=parsing-technical-formats * * @example * ```ts * Text.toDateTime('2026-05-04T09:53:51+03:00') * Text.toDateTime('04.05.2026', 'dd.MM.yyyy') * ``` */ toDateTime(dateString, template, opts) { if (type.Type.isStringFilled(template)) { return luxon.DateTime.fromFormat(dateString, template, opts); } return luxon.DateTime.fromISO(dateString, opts); } /** * Formats a date into the string Bitrix24 expects in REST payloads * (`yyyy-MM-dd'T'HH:mm:ssZZ`, i.e. PHP's `Y-m-d\TH:i:sP`). * * A string input is passed through unchanged (assumed already formatted); a * JS `Date` is converted through Luxon first. * * @param date - The value to format: an already-formatted string, a JS `Date`, * or a Luxon `DateTime`. * @returns The Bitrix24-formatted date string. * * @example * ```ts * Text.toB24Format(new Date()) // '2026-05-04T09:53:51+03:00' * ``` */ toB24Format(date) { if (typeof date === "string") { return date; } else if (date instanceof Date) { return this.toB24Format(luxon.DateTime.fromJSDate(date)); } return date.toFormat("yyyy-MM-dd'T'HH:mm:ssZZ"); } /** * Returns the current local timestamp formatted for log lines * (`yyyy-MM-dd HH:mm:ss`). * * @returns The formatted current timestamp. * * @example * ```ts * Text.getDateForLog() // '2026-05-04 09:53:51' * ``` */ getDateForLog() { return luxon.DateTime.now().toFormat("yyyy-MM-dd HH:mm:ss"); } /** * Serialises a plain object into an `application/x-www-form-urlencoded` * query string. * * Keys and values are percent-encoded. Array values are expanded into * indexed pairs (`key[0]=a&key[1]=b`). The leading `?` is **not** included. * * @param params - The object to serialise. A `null` / `undefined` value * yields an empty string. * @returns The encoded query string (without a leading `?`). * * @example * ```ts * Text.buildQueryString({ id: 7, tag: ['a', 'b'] }) * // 'id=7&tag%5B0%5D=a&tag%5B1%5D=b' * ``` */ buildQueryString(params) { if (type.Type.isNil(params)) { return ""; } const pairs = []; for (const [key, value] of Object.entries(params)) { if (type.Type.isArray(value)) { value.forEach((valueElement, index) => { pairs.push( `${encodeURIComponent(`${key}[${index}]`)}=${encodeURIComponent(valueElement)}` ); }); } else { pairs.push(`${encodeURIComponent(key)}=${encodeURIComponent(value)}`); } } return pairs.join("&"); } } const Text = new TextManager(); exports.Text = Text; //# sourceMappingURL=text.cjs.map