rsformat
Version:
Formatting/printing library for JavaScript that takes after rust's string formatting
368 lines (367 loc) • 13.9 kB
JavaScript
;
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;