UNPKG

rsformat

Version:

Formatting/printing library for JavaScript that takes after rust's string formatting

368 lines (367 loc) 13.9 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.style = exports.RsString = void 0; exports.buildString = buildString; exports.formatParam = formatParam; const node_util_1 = __importDefault(require("node:util")); const is_digit = (c) => c >= '0' && c <= '9'; const error = (param, char, reason) => new Error(`rs[param ${param}, char ${char}] ${reason}`); /** * Type representing a string formatted by `rs`. * An extension of `String`. */ class RsString extends String { /** * A version of the string that includes ANSI escape codes for debug formatting. */ colored; constructor(strings, params) { let { raw, colored } = buildString(strings, params); super(raw); this.colored = colored; } toString(debugColors = false) { if (debugColors) return this.colored; return this.valueOf(); } } exports.RsString = RsString; /** * Format a template literal with rust-style formatting and return it as a raw and colored string. * * @param strings String parts of the template * @param params Template parameters * * @returns An object with raw and colored versions of the formatted parameter */ function buildString(strings, params) { let raw = strings[0]; let colored = strings[0]; for (let i = 1; i < strings.length; ++i) { let string = strings[i]; let param = params[i - 1]; // Resolve parameter references recursively while (typeof param == 'object' && '__rs_param_ref' in param) { let ref_number = param.__rs_param_ref; if (typeof ref_number != 'number' || ref_number < 0 || ref_number >= params.length || !Number.isInteger(ref_number)) { throw new Error(`Parameter ${i - 1}: Invalid reference`); } if (ref_number == i - 1) throw new Error(`Parameter ${i - 1} references itself recursively`); param = params[param.__rs_param_ref]; } // Parse format specifier // If the string starts with a single : it has a format specifier, // If it has two the first : is being escaped and can be removed let skipParsing = false; if (string[0] == ':') { if (string[1] == ':') { string = string.substring(1); skipParsing = true; } } else skipParsing = true; if (skipParsing) { raw += param.toString(param instanceof String ? false : undefined) + string; colored += param.toString(param instanceof String ? true : undefined) + string; continue; } ; // Keep track of our index in the string to slice the format specifier later let idx = 1; // Compute format based on string let fill = ' ', align = '>', force_sign = '', pretty = false, pad_zeroes = false, width = 0, precision = -1, format_type = ''; // Fill/align // If the next character is align, then the current is the fill if (string[idx + 1] == '<' || string[idx + 1] == '^' || string[idx + 1] == '>') { fill = string[idx++]; } if (string[idx] == '<' || string[idx] == '^' || string[idx] == '>') { align = string[idx++]; } // Force sign if (string[idx] == '+' || string[idx] == '-') force_sign = string[idx++]; // Pretty formatting if (string[idx] == '#') pretty = true, idx++; // Padding numbers with zeroes if (string[idx] == '0') pad_zeroes = true, idx++; // Width if (is_digit(string[idx])) { let width_substring_start = idx++; while (is_digit(string[idx])) idx++; width = Number(string.substring(width_substring_start, idx)); } else if (idx == string.length && i < params.length) { // Grab the next parameter and fuse the string with the next one width = params[i]; if (typeof width != 'number') throw error(i - 1, idx, `Expected a number or number parameter for width specifier (found ${string[idx] ? "'" + string[idx] + "'" : typeof width + ' parameter'}).\nIf the next parameter was not meant to be a width number, add a : to the end of the formatting specifier.`); string += strings[++i]; } // Precision if (string[idx] == '.') { if (!is_digit(string[++idx])) { // Grab the next parameter and fuse the string with the next one precision = params[i]; if (typeof precision != 'number') throw error(i - 1, idx, `Expected a number or number parameter for precision specifier after . (found ${string[idx] ? "'" + string[idx] + "'" : typeof precision + ' parameter'}).\nIf the next parameter was not meant to be a precision number, add a : to the end of the formatting specifier.`); string += strings[++i]; } else { let precision_substring_start = idx; while (is_digit(string[idx])) idx++; precision = Number(string.substring(precision_substring_start, idx)); } } // Format type switch (string[idx]) { case '?': case 'o': case 'x': case 'X': case 'b': case 'e': case 'E': case 'n': case 'N': format_type = string[idx++]; } // End of specifier if (string[idx] == ':') { idx++; } else if (string[idx] != ' ' && string[idx] != '\t' && string[idx] != '\r' && string[idx] != '\n' && string[idx] !== undefined) { throw error(i - 1, idx, `Expected colon (':') or space character (' '/'\\t'/'\\r'/'\\n') at end of formatting specifier (found '${string[idx]}')`); } // Format parameter according to specifier let [formatted_colored, formatted_raw] = formatParam(param, { fill, align, force_sign, pretty, pad_zeroes, width, precision, type: format_type }); let escaped_string = string.substring(idx); colored += formatted_colored + escaped_string; raw += formatted_raw + escaped_string; } return { raw, colored }; } /** * Format a parameter as a string according to a specifier. * * @param param parameter to format * @param format format specifier object * @returns `param` as a debug-colored and raw formatted string */ function formatParam(param, format) { let param_type = typeof param; let base = 10; switch (format.type) { case 'o': base = 8; break; case 'x': case 'X': base = 16; break; case 'b': base = 2; break; } let param_raw; let param_colored = ""; // embed Strings directly if (param instanceof String && format.type != '?') { param_raw = param.toString(false); param_colored = param.toString(true); } else switch (format.type) { // Process format type case 'o': case 'x': case 'X': case 'b': param_raw = roundInBase(param, base, format.precision); if (format.type == "X") param_raw = param_raw.toUpperCase(); break; case 'e': case 'E': if (param_type != 'number' && param_type != 'bigint') { param_raw = param.toString(); break; } param_raw = param.toLocaleString('en-US', { notation: 'scientific', maximumFractionDigits: 20 }); if (format.type == 'e') param_raw = param_raw.toLowerCase(); // Do not pad with zeroes when using scientific formatting format.pad_zeroes = false; break; case 'n': case 'N': if (param_type != 'number' && param_type != 'bigint') { param_raw = param.toString(); break; } // Round and add suffix if (param_type == 'number') param = Math.round(param); param_raw = param.toString(); let last_2_digits = param_raw.substring(param_raw.length - 2); if (last_2_digits == '11' || last_2_digits == '12' || last_2_digits == '13') { param_raw += 'th'; } else switch (last_2_digits[last_2_digits.length - 1]) { case '1': param_raw += 'st'; break; case '2': param_raw += 'nd'; break; case '3': param_raw += 'rd'; break; default: param_raw += 'th'; } if (format.type == 'N') param_raw = param_raw.toUpperCase(); // Do not pad with zeroes when using ordinal formatting format.pad_zeroes = false; break; case '?': param_colored = node_util_1.default.inspect(param, { depth: Infinity, colors: true, compact: !format.pretty }); param_raw = node_util_1.default.stripVTControlCharacters(param_colored); // Do not force sign, pad with zeroes or align to precision when using debug formatting param_type = 'string'; // format.force_sign = ''; break; default: param_raw = roundInBase(param, base, format.precision); break; } ; if (param_type == 'string' && format.force_sign != '') { param_raw = format.force_sign == '+' ? param_raw.toUpperCase() : param_raw.toLowerCase(); } // let filled = false; if ((param_type == 'number') || (param_type == 'bigint')) { // Compute parameter sign let maybe_sign = param_raw.substring(0, 1); if (maybe_sign === '-') { param_raw = param_raw.substring(1, param_raw.length); } else if (format.force_sign == '+') { maybe_sign = '+'; } else if (format.force_sign == '-') { maybe_sign = ' '; } else { maybe_sign = ''; } // If pretty printing is enabled and the formating calls for a prefix, add it if (format.pretty) { switch (format.type) { case 'o': maybe_sign += '0o'; break; case 'x': case 'X': maybe_sign += '0x'; break; case 'b': maybe_sign += '0b'; break; } } //pad with zeroes if specified if (format.pad_zeroes) { // filled = true; while (param_raw.length < format.width - maybe_sign.length) { param_raw = '0' + param_raw; } } param_raw = maybe_sign + param_raw; } if (param_colored == "") param_colored = param_raw; let visible_length = [...param_raw].length; if (format.width > visible_length) { // Compute fill/align let left = ''; let right = ''; let diff = format.width - visible_length; switch (format.align) { case '>': left = format.fill.repeat(diff); break; case '<': right = format.fill.repeat(diff); break; case '^': left = format.fill.repeat(diff - diff / 2); // Prioritise right-aligment on uneven length right = format.fill.repeat(diff / 2 + diff % 2); break; } param_raw = left + param_raw + right; param_colored = left + param_colored + right; } return [param_colored, param_raw]; } /** * Helper function to round a number to a given precision in a given base. * Will also truncate strings to the desired length. */ function roundInBase(n, base, precision) { if (typeof n == "string") { if (precision == -1) return n; return n.slice(0, precision); } if (typeof n != "number" && typeof n != "bigint") { return n.toString(); } if (precision < 0) { return n.toString(base); } if (precision == 0) { return (typeof n == "bigint" ? n : Math.round(n)).toString(base); } const factor = base ** precision; const rounded = typeof n == "bigint" ? n * BigInt(factor) : Math.round((n + Number.EPSILON) * factor); const str = rounded.toString(base); // Insert radix point from the right const intPart = str.slice(0, -precision) || "0"; const fracPart = str.slice(-precision).padStart(precision, "0"); return intPart + "." + fracPart; } /** Re-export of node's `util.styleText`. */ exports.style = node_util_1.default.styleText;