771 lines
18 KiB
TypeScript
771 lines
18 KiB
TypeScript
// Supported specifiers set: [diuoxXaAfeEgGcspq%]
|
|
// Supported flags set: [-+0# ]
|
|
// Width and precision via digits or `*`
|
|
|
|
type FormatSpec = {
|
|
flags: number; // FLAG_*
|
|
width: number;
|
|
hasPrec: boolean;
|
|
prec: number;
|
|
spec: number;
|
|
};
|
|
|
|
const FLAG_MINUS = 1;
|
|
const FLAG_PLUS = 2;
|
|
const FLAG_ZERO = 4;
|
|
const FLAG_HASH = 8;
|
|
const FLAG_SPACE = 16;
|
|
|
|
function isDigit(c: number): boolean {
|
|
return c >= 48 && c <= 57; // '0'..'9'
|
|
}
|
|
|
|
// Parse a format spec starting after '%' and return index of specifier.
|
|
function parseSpec(
|
|
fmt: string,
|
|
start: number,
|
|
): { spec: FormatSpec; end: number } {
|
|
let i = start;
|
|
const len = fmt.length;
|
|
let flags = 0;
|
|
|
|
outer: while (i < len) {
|
|
switch (fmt.charCodeAt(i)) {
|
|
case 45:
|
|
flags |= FLAG_MINUS;
|
|
i++;
|
|
break; // '-'
|
|
case 43:
|
|
flags |= FLAG_PLUS;
|
|
i++;
|
|
break; // '+'
|
|
case 48:
|
|
flags |= FLAG_ZERO;
|
|
i++;
|
|
break; // '0'
|
|
case 35:
|
|
flags |= FLAG_HASH;
|
|
i++;
|
|
break; // '#'
|
|
case 32:
|
|
flags |= FLAG_SPACE;
|
|
i++;
|
|
break; // ' '
|
|
default:
|
|
break outer;
|
|
}
|
|
}
|
|
|
|
// Parse width
|
|
let width = 0;
|
|
if (i < len && fmt.charCodeAt(i) === 42) { // '*'
|
|
width = -1;
|
|
i++;
|
|
} else {
|
|
while (i < len && isDigit(fmt.charCodeAt(i))) {
|
|
width = width * 10 + (fmt.charCodeAt(i) - 48);
|
|
i++;
|
|
}
|
|
}
|
|
|
|
// Parse precision
|
|
let hasPrec = false;
|
|
let prec = 0;
|
|
if (i < len && fmt.charCodeAt(i) === 46) { // '.'
|
|
hasPrec = true;
|
|
i++;
|
|
if (i < len && fmt.charCodeAt(i) === 42) { // '*'
|
|
prec = -1;
|
|
i++;
|
|
} else {
|
|
while (i < len && isDigit(fmt.charCodeAt(i))) {
|
|
prec = prec * 10 + (fmt.charCodeAt(i) - 48);
|
|
i++;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Skip length modifiers [hlL] ignored in Lua
|
|
while (
|
|
i < len &&
|
|
(fmt.charCodeAt(i) === 104 || // 'h'
|
|
fmt.charCodeAt(i) === 108 || // 'l'
|
|
fmt.charCodeAt(i) === 76) // 'L'
|
|
) {
|
|
i++;
|
|
}
|
|
|
|
if (i >= len) {
|
|
throw new Error("invalid format (missing specifier)");
|
|
}
|
|
|
|
return {
|
|
spec: { flags, width, hasPrec, prec, spec: fmt.charCodeAt(i) },
|
|
end: i,
|
|
};
|
|
}
|
|
|
|
// pad a string to `width` respecting `FLAG_MINUS` and `FLAG_ZERO`
|
|
function pad(s: string, width: number, flags: number, numPad: boolean): string {
|
|
if (width <= 0 || s.length >= width) return s;
|
|
const n = width - s.length;
|
|
if (numPad && (flags & FLAG_ZERO) && !(flags & FLAG_MINUS)) {
|
|
let signLen = 0;
|
|
if (s.charCodeAt(0) === 45 || s.charCodeAt(0) === 43) { // '-' or '+'
|
|
signLen = 1;
|
|
} else if (
|
|
s.charCodeAt(0) === 48 &&
|
|
(s.charCodeAt(1) === 120 || s.charCodeAt(1) === 88)
|
|
) {
|
|
signLen = 2; // '0x' or '0X'
|
|
}
|
|
return s.slice(0, signLen) + "0".repeat(n) + s.slice(signLen);
|
|
}
|
|
if (flags & FLAG_MINUS) {
|
|
return s + " ".repeat(n);
|
|
}
|
|
return " ".repeat(n) + s;
|
|
}
|
|
|
|
function addSign(s: string, flags: number): string {
|
|
if (flags & FLAG_PLUS) return `+${s}`;
|
|
if (flags & FLAG_SPACE) return ` ${s}`;
|
|
return s;
|
|
}
|
|
|
|
function formatInt(n: number, spec: FormatSpec): string {
|
|
const code = spec.spec;
|
|
const v = Math.trunc(n);
|
|
|
|
let base = 10;
|
|
let unsigned = false;
|
|
let upper = false;
|
|
|
|
switch (code) {
|
|
case 100:
|
|
case 105: // 'd', 'i'
|
|
break;
|
|
case 117: // 'u'
|
|
unsigned = true;
|
|
break;
|
|
case 111: // 'o'
|
|
base = 8;
|
|
unsigned = true;
|
|
break;
|
|
case 120: // 'x'
|
|
base = 16;
|
|
unsigned = true;
|
|
break;
|
|
case 88: // 'X'
|
|
base = 16;
|
|
unsigned = true;
|
|
upper = true;
|
|
break;
|
|
}
|
|
|
|
let neg = false;
|
|
let digits: string;
|
|
|
|
if (unsigned && v < 0) {
|
|
// Reinterpret as 64-bit unsigned
|
|
const bv = BigInt(v) + (1n << 64n);
|
|
digits = bv.toString(base);
|
|
} else if (unsigned) {
|
|
digits = v.toString(base);
|
|
} else {
|
|
neg = v < 0;
|
|
digits = (neg ? -v : v).toString(base);
|
|
}
|
|
|
|
if (upper) digits = digits.toUpperCase();
|
|
|
|
// Precision
|
|
if (spec.hasPrec) {
|
|
if (spec.prec === 0 && v === 0) {
|
|
digits = "";
|
|
} else if (digits.length < spec.prec) {
|
|
digits = "0".repeat(spec.prec - digits.length) + digits;
|
|
}
|
|
}
|
|
|
|
// Alt flag
|
|
let prefix = "";
|
|
if (spec.flags & FLAG_HASH) {
|
|
if (base === 8 && (digits.length === 0 || digits.charCodeAt(0) !== 48)) {
|
|
prefix = "0";
|
|
} else if (base === 16 && v !== 0) {
|
|
prefix = upper ? "0X" : "0x";
|
|
}
|
|
}
|
|
|
|
let result: string;
|
|
if (neg) {
|
|
result = `-${prefix}${digits}`;
|
|
} else {
|
|
result = addSign(prefix + digits, spec.flags);
|
|
}
|
|
|
|
const numPad = !spec.hasPrec;
|
|
return pad(result, spec.width, spec.flags, numPad);
|
|
}
|
|
|
|
function formatFloat(n: number, spec: FormatSpec): string {
|
|
const code = spec.spec;
|
|
const upper = code === 69 || code === 71 || code === 70;
|
|
// 'E'=69 'G'=71 'F'=70 'e'=101 'g'=103 'f'=102
|
|
const lower = code | 32; // to lowercase
|
|
|
|
// Lua convention
|
|
if (!Number.isFinite(n)) {
|
|
let s: string;
|
|
if (n !== n) {
|
|
s = upper ? "-NAN" : "-nan";
|
|
} else if (n > 0) {
|
|
s = upper ? "INF" : "inf";
|
|
s = addSign(s, spec.flags);
|
|
} else {
|
|
s = upper ? "-INF" : "-inf";
|
|
}
|
|
return pad(s, spec.width, spec.flags, false);
|
|
}
|
|
|
|
const neg = n < 0 || (n === 0 && 1 / n === -Infinity);
|
|
const abs = neg ? -n : n;
|
|
const prec = spec.hasPrec ? spec.prec : 6;
|
|
|
|
let body: string;
|
|
|
|
if (lower === 102) { // 'f'
|
|
body = abs.toFixed(prec);
|
|
} else if (lower === 101) { // 'e'
|
|
body = abs.toExponential(prec);
|
|
// Ensure exponent has at least 2 digits
|
|
body = ensureExpTwoDigits(body);
|
|
} else { // 'g'
|
|
const gPrec = (prec === 0) ? 1 : prec;
|
|
if (abs === 0) {
|
|
body = "0";
|
|
} else {
|
|
// C rule: use 'e' if exponent < -4 or exponent >= precision
|
|
const exp = Math.floor(Math.log10(abs));
|
|
if (exp < -4 || exp >= gPrec) {
|
|
body = abs.toExponential(gPrec - 1);
|
|
body = ensureExpTwoDigits(body);
|
|
} else {
|
|
// Number of decimals = precision - (exponent + 1)
|
|
const decimals = gPrec - (exp + 1);
|
|
body = abs.toFixed(decimals);
|
|
}
|
|
}
|
|
// Strip trailing zeros unless '#' flag
|
|
if (!(spec.flags & FLAG_HASH)) {
|
|
body = stripTrailingZerosG(body);
|
|
}
|
|
}
|
|
|
|
if (upper) {
|
|
body = body.toUpperCase();
|
|
}
|
|
|
|
// Alt flag for 'f'/'e': ensure decimal point exists
|
|
if ((spec.flags & FLAG_HASH) && lower !== 103) {
|
|
if (body.indexOf(".") === -1) {
|
|
// Insert dot before 'e' if present, else append
|
|
const eIdx = body.indexOf("e");
|
|
const EIdx = body.indexOf("E");
|
|
const expIdx = eIdx !== -1 ? eIdx : EIdx;
|
|
if (expIdx !== -1) {
|
|
body = `${body.slice(0, expIdx)}.${body.slice(expIdx)}`;
|
|
} else {
|
|
body = `${body}.`;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Alt flag for 'g': keep trailing zeros but ensure decimal point
|
|
if ((spec.flags & FLAG_HASH) && lower === 103) {
|
|
if (body.indexOf(".") === -1) {
|
|
const expIdx = findExpIndex(body);
|
|
if (expIdx !== -1) {
|
|
body = `${body.slice(0, expIdx)}.${body.slice(expIdx)}`;
|
|
} else {
|
|
body = `${body}.`;
|
|
}
|
|
}
|
|
}
|
|
|
|
let result: string;
|
|
if (neg) {
|
|
result = `-${body}`;
|
|
} else {
|
|
result = addSign(body, spec.flags);
|
|
}
|
|
|
|
return pad(result, spec.width, spec.flags, true);
|
|
}
|
|
|
|
function findExpIndex(s: string): number {
|
|
for (let i = 0; i < s.length; i++) {
|
|
const c = s.charCodeAt(i);
|
|
if (c === 101 || c === 69) return i; // 'e' or 'E'
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
// Ensure exponent part has at least 2 digits
|
|
function ensureExpTwoDigits(s: string): string {
|
|
const idx = findExpIndex(s);
|
|
if (idx === -1) return s;
|
|
// idx+1 is sign, idx+2... are digits
|
|
const signIdx = idx + 1;
|
|
if (signIdx >= s.length) return s;
|
|
const digitStart = signIdx + 1;
|
|
const expLen = s.length - digitStart;
|
|
if (expLen < 2) {
|
|
return `${s.slice(0, digitStart)}0${s.slice(digitStart)}`;
|
|
}
|
|
return s;
|
|
}
|
|
|
|
// Strip trailing zeros from '%g' output
|
|
function stripTrailingZerosG(s: string): string {
|
|
const expIdx = findExpIndex(s);
|
|
const mantissa = expIdx !== -1 ? s.slice(0, expIdx) : s;
|
|
const exp = expIdx !== -1 ? s.slice(expIdx) : "";
|
|
|
|
const dotIdx = mantissa.indexOf(".");
|
|
if (dotIdx === -1) return s; // nothing to strip
|
|
|
|
let end = mantissa.length;
|
|
while (end > dotIdx + 1 && mantissa.charCodeAt(end - 1) === 48) { // '0'
|
|
end--;
|
|
}
|
|
// Remove dot if nothing after it
|
|
if (end === dotIdx + 1) {
|
|
end = dotIdx;
|
|
}
|
|
|
|
return mantissa.slice(0, end) + exp;
|
|
}
|
|
|
|
// Format a number as hexadecimal floating-point (%a/%A)
|
|
function formatHexFloat(n: number, spec: FormatSpec): string {
|
|
const code = spec.spec;
|
|
const upper = code === 65; // 'A'
|
|
|
|
if (!Number.isFinite(n)) {
|
|
let s: string;
|
|
if (n !== n) {
|
|
s = upper ? "-NAN" : "-nan";
|
|
} else if (n > 0) {
|
|
s = upper ? "INF" : "inf";
|
|
s = addSign(s, spec.flags);
|
|
} else {
|
|
s = upper ? "-INF" : "-inf";
|
|
}
|
|
return pad(s, spec.width, spec.flags, false);
|
|
}
|
|
|
|
const neg = n < 0 || (n === 0 && 1 / n === -Infinity);
|
|
const abs = neg ? -n : n;
|
|
|
|
let body: string;
|
|
if (abs === 0) {
|
|
const prec = spec.hasPrec ? spec.prec : 0;
|
|
if (prec > 0) {
|
|
body = `0x0.${"0".repeat(prec)}p+0`;
|
|
} else {
|
|
body = "0x0p+0";
|
|
}
|
|
} else {
|
|
body = hexFloatBody(abs, spec);
|
|
}
|
|
|
|
if (upper) body = body.toUpperCase();
|
|
|
|
// Alt flag: ensure decimal point
|
|
if (spec.flags & FLAG_HASH) {
|
|
const pIdx = findPIndex(body);
|
|
if (pIdx !== -1) {
|
|
let hasDot = false;
|
|
for (let k = 0; k < pIdx; k++) {
|
|
if (body.charCodeAt(k) === 46) { // '.'
|
|
hasDot = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!hasDot) {
|
|
body = `${body.slice(0, pIdx)}.${body.slice(pIdx)}`;
|
|
}
|
|
}
|
|
}
|
|
|
|
let result: string;
|
|
if (neg) {
|
|
result = `-${body}`;
|
|
} else {
|
|
result = addSign(body, spec.flags);
|
|
}
|
|
|
|
return pad(result, spec.width, spec.flags, true);
|
|
}
|
|
|
|
// Find index of 'p' or 'P' in hex float string
|
|
function findPIndex(s: string): number {
|
|
for (let i = 0; i < s.length; i++) {
|
|
const c = s.charCodeAt(i);
|
|
if (c === 112 || c === 80) return i; // 'p' or 'P'
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
// Number of bits needed to represent a positive bigint
|
|
function bitLength(n: bigint): number {
|
|
let bits = 0;
|
|
let v = n;
|
|
while (v > 0n) {
|
|
bits++;
|
|
v >>= 1n;
|
|
}
|
|
return bits;
|
|
}
|
|
|
|
// Decompose a positive non-zero finite float into `0xH.HHHpN` form
|
|
function hexFloatBody(abs: number, spec: FormatSpec): string {
|
|
const buf = new Float64Array(1);
|
|
const view = new DataView(buf.buffer);
|
|
view.setFloat64(0, abs);
|
|
const bits = view.getBigUint64(0);
|
|
const biasedExp = Number((bits >> 52n) & 0x7FFn);
|
|
const frac = bits & 0xFFFFFFFFFFFFFn;
|
|
|
|
let exponent: number;
|
|
let mantBits: bigint;
|
|
|
|
if (biasedExp === 0) {
|
|
// Subnormal
|
|
if (frac === 0n) return "0x0p+0";
|
|
const shift = 52 - bitLength(frac) + 1;
|
|
mantBits = frac << BigInt(shift);
|
|
exponent = -1022 - shift;
|
|
} else {
|
|
// Normal
|
|
exponent = biasedExp - 1023;
|
|
mantBits = frac | (1n << 52n);
|
|
}
|
|
|
|
let firstDigit = Number(mantBits >> 52n);
|
|
const restBits = mantBits & ((1n << 52n) - 1n);
|
|
|
|
// 13 hex digits from 52 bits
|
|
let fracHex = hexDigits52(restBits);
|
|
|
|
if (spec.hasPrec) {
|
|
if (spec.prec < 13) {
|
|
const carry = roundHexInPlace(fracHex, spec.prec);
|
|
if (carry) firstDigit++;
|
|
fracHex = truncHexDigits(fracHex, spec.prec);
|
|
} else {
|
|
fracHex = padHexRight(fracHex, spec.prec);
|
|
}
|
|
} else {
|
|
fracHex = stripHexTrailingZeros(fracHex);
|
|
}
|
|
|
|
const expSign = exponent >= 0 ? "+" : "";
|
|
if (fracHex.length > 0) {
|
|
return `0x${firstDigit}.${fracHex}p${expSign}${exponent}`;
|
|
}
|
|
return `0x${firstDigit}p${expSign}${exponent}`;
|
|
}
|
|
|
|
// Convert 52-bit value to 13 hex digits, zero-padded
|
|
function hexDigits52(bits: bigint): string {
|
|
const s = bits.toString(16);
|
|
if (s.length >= 13) return s;
|
|
return "0".repeat(13 - s.length) + s;
|
|
}
|
|
|
|
// Parse one hex char to its numeric value
|
|
function hexVal(c: number): number {
|
|
if (c >= 48 && c <= 57) return c - 48; // '0'..'9'
|
|
if (c >= 97 && c <= 102) return c - 87; // 'a'..'f'
|
|
if (c >= 65 && c <= 70) return c - 55; // 'A'..'F'
|
|
return 0;
|
|
}
|
|
|
|
function roundHexInPlace(digits: string, prec: number): boolean {
|
|
if (prec >= digits.length) return false;
|
|
|
|
const nextVal = hexVal(digits.charCodeAt(prec));
|
|
if (nextVal < 8) return false;
|
|
|
|
if (prec === 0) return true;
|
|
|
|
const arr = new Array<number>(prec);
|
|
for (let i = 0; i < prec; i++) {
|
|
arr[i] = hexVal(digits.charCodeAt(i));
|
|
}
|
|
|
|
let carry = 1;
|
|
for (let i = prec - 1; i >= 0 && carry; i--) {
|
|
arr[i] += carry;
|
|
if (arr[i] >= 16) {
|
|
arr[i] = 0;
|
|
carry = 1;
|
|
} else {
|
|
carry = 0;
|
|
}
|
|
}
|
|
|
|
return carry === 1;
|
|
}
|
|
|
|
function truncHexDigits(digits: string, prec: number): string {
|
|
if (prec === 0) return "";
|
|
|
|
const nextVal = hexVal(digits.charCodeAt(prec));
|
|
if (nextVal < 8) return digits.slice(0, prec);
|
|
|
|
const arr = new Array<number>(prec);
|
|
for (let i = 0; i < prec; i++) {
|
|
arr[i] = hexVal(digits.charCodeAt(i));
|
|
}
|
|
|
|
let carry = 1;
|
|
for (let i = prec - 1; i >= 0 && carry; i--) {
|
|
arr[i] += carry;
|
|
if (arr[i] >= 16) {
|
|
arr[i] = 0;
|
|
} else {
|
|
carry = 0;
|
|
}
|
|
}
|
|
|
|
let out = "";
|
|
for (let i = 0; i < prec; i++) {
|
|
out += arr[i].toString(16);
|
|
}
|
|
|
|
return out;
|
|
}
|
|
|
|
function padHexRight(s: string, len: number): string {
|
|
if (s.length >= len) return s;
|
|
return s + "0".repeat(len - s.length);
|
|
}
|
|
|
|
function stripHexTrailingZeros(s: string): string {
|
|
let end = s.length;
|
|
while (end > 0 && s.charCodeAt(end - 1) === 48) { // '0'
|
|
end--;
|
|
}
|
|
if (end === s.length) return s;
|
|
return s.slice(0, end);
|
|
}
|
|
|
|
function quoteString(s: string): string {
|
|
let out = '"';
|
|
for (let i = 0; i < s.length; i++) {
|
|
const c = s.charCodeAt(i);
|
|
if (c === 34 || c === 92 || c === 10) {
|
|
// '"', '\\', '\n': backslash + literal char
|
|
out += "\\";
|
|
out += String.fromCharCode(c);
|
|
} else if (c < 32) {
|
|
const next = i + 1 < s.length ? s.charCodeAt(i + 1) : -1;
|
|
const isNextDigit = next >= 48 && next <= 57;
|
|
if (isNextDigit) {
|
|
const ds = c.toString();
|
|
out += "\\";
|
|
if (ds.length < 3) out += "0".repeat(3 - ds.length);
|
|
out += ds;
|
|
} else {
|
|
out += `\\${c.toString()}`;
|
|
}
|
|
} else {
|
|
out += String.fromCharCode(c);
|
|
}
|
|
}
|
|
out += '"';
|
|
return out;
|
|
}
|
|
|
|
// Format a float for %q: hex representation preserving full precision
|
|
function quoteFloat(n: number): string {
|
|
if (n !== n) return "(0/0)";
|
|
if (n === Infinity) return "1e9999";
|
|
if (n === -Infinity) return "-1e9999";
|
|
|
|
const spec: FormatSpec = {
|
|
flags: 0,
|
|
width: 0,
|
|
hasPrec: false,
|
|
prec: 0,
|
|
spec: 97, // 'a'
|
|
};
|
|
return formatHexFloat(n, spec);
|
|
}
|
|
|
|
function formatQ(v: unknown): string {
|
|
if (v === null || v === undefined) return "nil";
|
|
if (v === true) return "true";
|
|
if (v === false) return "false";
|
|
|
|
if (typeof v === "number") {
|
|
if (v === 0 && 1 / v === -Infinity) return quoteFloat(v);
|
|
if (Number.isInteger(v) && Number.isFinite(v)) {
|
|
return v.toString();
|
|
}
|
|
return quoteFloat(v);
|
|
}
|
|
|
|
return quoteString(String(v));
|
|
}
|
|
|
|
function formatChar(n: number): string {
|
|
return String.fromCharCode(n & 0x7f);
|
|
}
|
|
|
|
const objectIds = new WeakMap<WeakKey, number>();
|
|
const stringIds = new Map<string, number>();
|
|
let nextId = 1;
|
|
|
|
function toPointer(v: unknown): string {
|
|
if (v === null || v === undefined) return "(null)";
|
|
if (typeof v === "boolean" || typeof v === "number") return "(null)";
|
|
|
|
// Primitives (strings, symbols, etc.) cannot be `WeakMap` keys
|
|
if (typeof v !== "object" && typeof v !== "function") {
|
|
const key = String(v);
|
|
let id = stringIds.get(key);
|
|
if (id === undefined) {
|
|
id = nextId++;
|
|
stringIds.set(key, id);
|
|
}
|
|
return `0x${id.toString(16).padStart(14, "0")}`;
|
|
}
|
|
|
|
const obj = v as object;
|
|
let id = objectIds.get(obj);
|
|
if (id === undefined) {
|
|
id = nextId++;
|
|
objectIds.set(obj, id);
|
|
}
|
|
return `0x${id.toString(16).padStart(14, "0")}`;
|
|
}
|
|
|
|
function formatPointer(v: unknown, spec: FormatSpec): string {
|
|
const s = toPointer(v);
|
|
// `%p` only supports width and '-' flag, no precision
|
|
return pad(s, spec.width, spec.flags, false);
|
|
}
|
|
|
|
export function luaFormat(fmt: string, ...args: any[]): string {
|
|
let out = "";
|
|
let ai = 0; // arg index
|
|
const len = fmt.length;
|
|
let i = 0;
|
|
|
|
while (i < len) {
|
|
const c = fmt.charCodeAt(i);
|
|
if (c !== 37) { // not '%'
|
|
// Fast path: scan for next '%' or end
|
|
let j = i + 1;
|
|
while (j < len && fmt.charCodeAt(j) !== 37) j++;
|
|
out += fmt.slice(i, j);
|
|
i = j;
|
|
continue;
|
|
}
|
|
|
|
// '%' found
|
|
i++;
|
|
if (i >= len) {
|
|
throw new Error("invalid format (ends with '%')");
|
|
}
|
|
|
|
// '%%' into literal '%'
|
|
if (fmt.charCodeAt(i) === 37) {
|
|
out += "%";
|
|
i++;
|
|
continue;
|
|
}
|
|
|
|
const { spec, end } = parseSpec(fmt, i);
|
|
i = end + 1;
|
|
|
|
// Resolve `*` width and precision from args
|
|
let width = spec.width;
|
|
if (width === -1) {
|
|
width = Number(args[ai++]) || 0;
|
|
if (width < 0) {
|
|
spec.flags |= FLAG_MINUS;
|
|
width = -width;
|
|
}
|
|
spec.width = width;
|
|
}
|
|
if (spec.prec === -1) {
|
|
let p = Number(args[ai++]) || 0;
|
|
if (p < 0) {
|
|
spec.hasPrec = false;
|
|
p = 0;
|
|
}
|
|
spec.prec = p;
|
|
}
|
|
|
|
const code = spec.spec;
|
|
switch (code) {
|
|
case 97:
|
|
case 65: // 'a', 'A'
|
|
out += formatHexFloat(Number(args[ai++]), spec);
|
|
break;
|
|
case 100:
|
|
case 105:
|
|
case 117: // 'd', 'i', 'u'
|
|
case 111:
|
|
case 120:
|
|
case 88: // 'o', 'x', 'X'
|
|
out += formatInt(Number(args[ai++]), spec);
|
|
break;
|
|
case 102:
|
|
case 101:
|
|
case 69: // 'f', 'e', 'E'
|
|
case 103:
|
|
case 71:
|
|
case 70: // 'g', 'G', 'F'
|
|
out += formatFloat(Number(args[ai++]), spec);
|
|
break;
|
|
case 99: // 'c'
|
|
out += pad(
|
|
formatChar(Number(args[ai++])),
|
|
spec.width,
|
|
spec.flags,
|
|
false,
|
|
);
|
|
break;
|
|
case 112: { // 'p'
|
|
out += formatPointer(args[ai++], spec);
|
|
break;
|
|
}
|
|
case 113: { // 'q'
|
|
out += formatQ(args[ai++]);
|
|
break;
|
|
}
|
|
case 115: { // 's'
|
|
let s = String(args[ai++]);
|
|
if (spec.hasPrec && s.length > spec.prec) {
|
|
s = s.slice(0, spec.prec);
|
|
}
|
|
out += pad(s, spec.width, spec.flags, false);
|
|
break;
|
|
}
|
|
default:
|
|
throw new Error(
|
|
`invalid format specifier '${String.fromCharCode(code)}'`,
|
|
);
|
|
}
|
|
}
|
|
|
|
return out;
|
|
}
|