1/* * 2 * 3 * (c) 2010-2024 Torstein Honsi 4 * 5 * License: www.highcharts.com/license 6 * 7 * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!! 8 * 9 * */ 10'use strict'; 11import D from './Defaults.js'; 12const { defaultOptions, defaultTime } = D; 13import G from './Globals.js'; 14const { pageLang } = G; 15import U from './Utilities.js'; 16const { extend, getNestedProperty, isArray, isNumber, isObject, isString, pick, ucfirst } = U; 17const helpers = { 18 // Built-in helpers 19 add: (a, b) => a + b, 20 divide: (a, b) => (b !== 0 ? a / b : ''), 21 // eslint-disable-next-line eqeqeq 22 eq: (a, b) => a == b, 23 each: function (arr) { 24 const match = arguments[arguments.length - 1]; 25 return isArray(arr) ? 26 arr.map((item, i) => format(match.body, extend(isObject(item) ? item : { '@this': item }, { 27 '@index': i, 28 '@first': i === 0, 29 '@last': i === arr.length - 1 30 }))).join('') : 31 false; 32 }, 33 ge: (a, b) => a >= b, 34 gt: (a, b) => a > b, 35 'if': (condition) => !!condition, 36 le: (a, b) => a <= b, 37 lt: (a, b) => a < b, 38 multiply: (a, b) => a * b, 39 // eslint-disable-next-line eqeqeq 40 ne: (a, b) => a != b, 41 subtract: (a, b) => a - b, 42 ucfirst, 43 unless: (condition) => !condition 44}; 45const numberFormatCache = {}; 46/* * 47 * 48 * Functions 49 * 50 * */ 51// Internal convenience function 52const isQuotedString = (str) => /^["'].+["']$/.test(str); 53/** 54 * Formats a JavaScript date timestamp (milliseconds since Jan 1st 1970) into a 55 * human readable date string. The format is a subset of the formats for PHP's 56 * [strftime](https://www.php.net/manual/en/function.strftime.php) function. 57 * Additional formats can be given in the {@link Highcharts.dateFormats} hook. 58 * 59 * Since v6.0.5, all internal dates are formatted through the 60 * {@link Highcharts.Chart#time} instance to respect chart-level time settings. 61 * The `Highcharts.dateFormat` function only reflects global time settings set 62 * with `setOptions`. 63 * 64 * Supported format keys: 65 * - `%a`: Short weekday, like 'Mon' 66 * - `%A`: Long weekday, like 'Monday' 67 * - `%d`: Two digit day of the month, 01 to 31 68 * - `%e`: Day of the month, 1 through 31 69 * - `%w`: Day of the week, 0 through 6 70 * - `%b`: Short month, like 'Jan' 71 * - `%B`: Long month, like 'January' 72 * - `%m`: Two digit month number, 01 through 12 73 * - `%y`: Two digits year, like 09 for 2009 74 * - `%Y`: Four digits year, like 2009 75 * - `%H`: Two digits hours in 24h format, 00 through 23 76 * - `%k`: Hours in 24h format, 0 through 23 77 * - `%I`: Two digits hours in 12h format, 00 through 11 78 * - `%l`: Hours in 12h format, 1 through 12 79 * - `%M`: Two digits minutes, 00 through 59 80 * - `%p`: Upper case AM or PM 81 * - `%P`: Lower case AM or PM 82 * - `%S`: Two digits seconds, 00 through 59 83 * - `%L`: Milliseconds (naming from Ruby) 84 * 85 * @function Highcharts.dateFormat 86 * 87 * @param {string} format 88 * The desired format where various time representations are prefixed 89 * with `%`. 90 * 91 * @param {number} timestamp 92 * The JavaScript timestamp. 93 * 94 * @param {boolean} [upperCaseFirst=false] 95 * Upper case first letter in the return. 96 * 97 * @return {string} 98 * The formatted date. 99 */ 100function dateFormat(format, timestamp, upperCaseFirst) { 101 return defaultTime.dateFormat(format, timestamp, upperCaseFirst); 102} 103/** 104 * Format a string according to a subset of the rules of Python's String.format 105 * method. 106 * 107 * @example 108 * let s = Highcharts.format( 109 * 'The {color} fox was {len:.2f} feet long', 110 * { color: 'red', len: Math.PI } 111 * ); 112 * // => The red fox was 3.14 feet long 113 * 114 * @function Highcharts.format 115 * 116 * @param {string} str 117 * The string to format. 118 * 119 * @param {Record<string, *>} ctx 120 * The context, a collection of key-value pairs where each key is 121 * replaced by its value. 122 * 123 * @param {Highcharts.Chart} [chart] 124 * A `Chart` instance used to get numberFormatter and time. 125 * 126 * @return {string} 127 * The formatted string. 128 */ 129function format(str = '', ctx, chart) { 130 const regex = /\{([\p{L}\d:\.,;\-\/<>\[\]%_@+"'â= #\(\)]+)\}/gu, 131 // The sub expression regex is the same as the top expression regex, 132 // but except parens and block helpers (#), and surrounded by parens 133 // instead of curly brackets. 134 subRegex = /\(([\p{L}\d:\.,;\-\/<>\[\]%_@+"'= ]+)\)/gu, matches = [], floatRegex = /f$/, decRegex = /\.(\d)/, lang = chart?.options.lang || defaultOptions.lang, time = chart && chart.time || defaultTime, numberFormatter = chart && chart.numberFormatter || numberFormat; 135 /* 136 * Get a literal or variable value inside a template expression. May be 137 * extended with other types like string or null if needed, but keep it 138 * small for now. 139 */ 140 const resolveProperty = (key = '') => { 141 let n; 142 // Literals 143 if (key === 'true') { 144 return true; 145 } 146 if (key === 'false') { 147 return false;
vendor: 9,481 bytes, lines 148-401
148 } 149 if ((n = Number(key)).toString() === key) { 150 return n; 151 } 152 if (isQuotedString(key)) { 153 return key.slice(1, -1); 154 } 155 // Variables and constants 156 return getNestedProperty(key, ctx); 157 }; 158 let match, currentMatch, depth = 0, hasSub; 159 // Parse and create tree 160 while ((match = regex.exec(str)) !== null) { 161 // When a sub expression is found, it is evaluated first, and the 162 // results recursively evaluated until no subexpression exists. 163 const mainMatch = match, subMatch = subRegex.exec(match[1]); 164 if (subMatch) { 165 match = subMatch; 166 hasSub = true; 167 } 168 if (!currentMatch || !currentMatch.isBlock) { 169 currentMatch = { 170 ctx, 171 expression: match[1], 172 find: match[0], 173 isBlock: match[1].charAt(0) === '#', 174 start: match.index, 175 startInner: match.index + match[0].length, 176 length: match[0].length 177 }; 178 } 179 // Identify helpers 180 const fn = (currentMatch.isBlock ? mainMatch : match)[1].split(' ')[0].replace('#', ''); 181 if (helpers[fn]) { 182 // Block helper, only 0 level is handled 183 if (currentMatch.isBlock && fn === currentMatch.fn) { 184 depth++; 185 } 186 if (!currentMatch.fn) { 187 currentMatch.fn = fn; 188 } 189 } 190 // Closing a block helper 191 const startingElseSection = match[1] === 'else'; 192 if (currentMatch.isBlock && 193 currentMatch.fn && (match[1] === `/${currentMatch.fn}` || 194 startingElseSection)) { 195 if (!depth) { // === 0 196 const start = currentMatch.startInner, body = str.substr(start, match.index - start); 197 // Either closing without an else section, or when encountering 198 // an else section 199 if (currentMatch.body === void 0) { 200 currentMatch.body = body; 201 currentMatch.startInner = match.index + match[0].length; 202 // The body exists already, so this is the else section 203 } 204 else { 205 currentMatch.elseBody = body; 206 } 207 currentMatch.find += body + match[0]; 208 if (!startingElseSection) { 209 matches.push(currentMatch); 210 currentMatch = void 0; 211 } 212 } 213 else if (!startingElseSection) { 214 depth--; 215 } 216 // Common expression 217 } 218 else if (!currentMatch.isBlock) { 219 matches.push(currentMatch); 220 } 221 // Evaluate sub-matches one by one to prevent orphaned block closers 222 if (subMatch && !currentMatch?.isBlock) { 223 break; 224 } 225 } 226 // Execute 227 matches.forEach((match) => { 228 const { body, elseBody, expression, fn } = match; 229 let replacement, i; 230 // Helper function 231 if (fn) { 232 // Pass the helpers the amount of arguments defined by the function, 233 // then the match as the last argument. 234 const args = [match], parts = [], len = expression.length; 235 let start = 0, startChar; 236 for (i = 0; i <= len; i++) { 237 const char = expression.charAt(i); 238 // Start of string 239 if (!startChar && (char === '"' || char === '\'')) { 240 startChar = char; 241 // End of string 242 } 243 else if (startChar === char) { 244 startChar = ''; 245 } 246 if (!startChar && 247 (char === ' ' || i === len)) { 248 parts.push(expression.substr(start, i - start)); 249 start = i + 1; 250 } 251 } 252 i = helpers[fn].length; 253 while (i--) { 254 args.unshift(resolveProperty(parts[i + 1])); 255 } 256 replacement = helpers[fn].apply(ctx, args); 257 // Block helpers may return true or false. They may also return a 258 // string, like the `each` helper. 259 if (match.isBlock && typeof replacement === 'boolean') { 260 replacement = format(replacement ? body : elseBody, ctx, chart); 261 } 262 // Simple variable replacement 263 } 264 else { 265 const valueAndFormat = isQuotedString(expression) ? 266 [expression] : expression.split(':'); 267 replacement = resolveProperty(valueAndFormat.shift() || ''); 268 // Format the replacement 269 if (valueAndFormat.length && typeof replacement === 'number') { 270 const segment = valueAndFormat.join(':'); 271 if (floatRegex.test(segment)) { // Float 272 const decimals = parseInt((segment.match(decRegex) || ['', '-1'])[1], 10); 273 if (replacement !== null) { 274 replacement = numberFormatter(replacement, decimals, lang.decimalPoint, segment.indexOf(',') > -1 ? lang.thousandsSep : ''); 275 } 276 } 277 else { 278 replacement = time.dateFormat(segment, replacement); 279 } 280 } 281 // Use string literal in order to be preserved in the outer 282 // expression 283 subRegex.lastIndex = 0; 284 if (subRegex.test(match.find) && isString(replacement)) { 285 replacement = `"${replacement}"`; 286 } 287 } 288 str = str.replace(match.find, pick(replacement, '')); 289 }); 290 return hasSub ? format(str, ctx, chart) : str; 291} 292/** 293 * Format a number and return a string based on input settings. 294 * 295 * @sample highcharts/members/highcharts-numberformat/ 296 * Custom number format 297 * 298 * @function Highcharts.numberFormat 299 * 300 * @param {number} number 301 * The input number to format. 302 * 303 * @param {number} decimals 304 * The amount of decimals. A value of -1 preserves the amount in the 305 * input number. 306 * 307 * @param {string} [decimalPoint] 308 * The decimal point, defaults to the one given in the lang options, or 309 * a dot. 310 * 311 * @param {string} [thousandsSep] 312 * The thousands separator, defaults to the one given in the lang 313 * options, or a space character. 314 * 315 * @return {string} 316 * The formatted number. 317 */ 318function numberFormat(number, decimals, decimalPoint, thousandsSep) { 319 number = +number || 0; 320 decimals = +decimals; 321 let ret, fractionDigits, [mantissa, exp] = number.toString().split('e').map(Number); 322 const lang = this?.options?.lang || defaultOptions.lang, origDec = (number.toString().split('.')[1] || '').split('e')[0].length, firstDecimals = decimals, options = {}; 323 decimalPoint ?? (decimalPoint = lang.decimalPoint); 324 thousandsSep ?? (thousandsSep = lang.thousandsSep); 325 if (decimals === -1) { 326 // Preserve decimals. Not huge numbers (#3793). 327 decimals = Math.min(origDec, 20); 328 } 329 else if (!isNumber(decimals)) { 330 decimals = 2; 331 } 332 else if (decimals && exp < 0) { 333 // Expose decimals from exponential notation (#7042) 334 fractionDigits = decimals + exp; 335 if (fractionDigits >= 0) { 336 // Remove too small part of the number while keeping the notation 337 mantissa = +mantissa.toExponential(fractionDigits).split('e')[0]; 338 decimals = fractionDigits; 339 } 340 else { 341 // `fractionDigits < 0` 342 mantissa = Math.floor(mantissa); 343 if (decimals < 20) { 344 // Use number instead of exponential notation (#7405) 345 number = +(mantissa * Math.pow(10, exp)).toFixed(decimals); 346 } 347 else { 348 // Or zero 349 number = 0; 350 } 351 exp = 0; 352 } 353 } 354 if (exp) { 355 decimals ?? (decimals = 2); 356 number = mantissa; 357 } 358 if (isNumber(decimals) && decimals >= 0) { 359 options.minimumFractionDigits = decimals; 360 options.maximumFractionDigits = decimals; 361 } 362 if (thousandsSep === '') { 363 options.useGrouping = false; 364 } 365 const hasSeparators = thousandsSep || decimalPoint, locale = hasSeparators ? 366 'en' : 367 (this?.locale || lang.locale || pageLang), cacheKey = JSON.stringify(options) + locale, nf = numberFormatCache[cacheKey] ?? (numberFormatCache[cacheKey] = new Intl.NumberFormat(locale, options)); 368 ret = nf.format(number); 369 // If thousandsSep or decimalPoint are set, fall back to using English 370 // format with string replacement for the separators. 371 if (hasSeparators) { 372 ret = ret 373 // Preliminary step to avoid re-swapping (#22402) 374 .replace(/([,\.])/g, '_$1') 375 .replace(/_\,/g, thousandsSep ?? ',') 376 .replace('_.', decimalPoint ?? '.'); 377 } 378 if ( 379 // Remove signed zero (#20564) 380 (!decimals && +ret === 0) || 381 // Small numbers, no decimals (#14023) 382 (exp < 0 && !firstDecimals)) { 383 ret = '0'; 384 } 385 if (exp && +ret !== 0) { 386 ret += 'e' + (exp < 0 ? '' : '+') + exp; 387 } 388 return ret; 389} 390/* * 391 * 392 * Default Export 393 * 394 * */ 395const Templating = { 396 dateFormat, 397 format, 398 helpers, 399 numberFormat 400}; 401export default Templating;
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.