format() that works with RA pipe codes & ANSI ESC seqs
This commit is contained in:
parent
7ce2a3bbe5
commit
9bd39f6d80
|
@ -0,0 +1,327 @@
|
|||
/* jslint node: true */
|
||||
'use strict';
|
||||
|
||||
const EnigError = require('./enig_error.js').EnigError;
|
||||
const pad = require('./string_util.js').pad;
|
||||
const stylizeString = require('./string_util.js').stylizeString;
|
||||
|
||||
// deps
|
||||
const _ = require('lodash');
|
||||
|
||||
/*
|
||||
String formatting HEAVILY inspired by David Chambers string-format library
|
||||
and the mini-language branch specifically which was gratiously released
|
||||
under the DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE.
|
||||
|
||||
We need some extra functionality. Namely, support for RA style pipe codes
|
||||
and ANSI escape sequences.
|
||||
*/
|
||||
|
||||
class ValueError extends EnigError { }
|
||||
class KeyError extends EnigError { }
|
||||
|
||||
const SpecRegExp = {
|
||||
FillAlign : /^(.)?([<>=^])/,
|
||||
Sign : /^[ +-]/,
|
||||
Width : /^\d*/,
|
||||
Precision : /^\d+/,
|
||||
};
|
||||
|
||||
function tokenizeFormatSpec(spec) {
|
||||
const tokens = {
|
||||
fill : '',
|
||||
align : '',
|
||||
sign : '',
|
||||
'#' : false,
|
||||
'0' : false,
|
||||
width : '',
|
||||
',' : false,
|
||||
precision : '',
|
||||
type : '',
|
||||
};
|
||||
|
||||
let index = 0;
|
||||
let match;
|
||||
|
||||
function incIndexByMatch() {
|
||||
index += match[0].length;
|
||||
}
|
||||
|
||||
match = SpecRegExp.FillAlign.exec(spec);
|
||||
if(match) {
|
||||
if(match[1]) {
|
||||
tokens.fill = match[1];
|
||||
}
|
||||
tokens.align = match[2];
|
||||
incIndexByMatch();
|
||||
}
|
||||
|
||||
match = SpecRegExp.Sign.exec(spec.slice(index));
|
||||
if(match) {
|
||||
tokens.sign = match[0];
|
||||
incIndexByMatch();
|
||||
}
|
||||
|
||||
if('#' === spec.charAt(index)) {
|
||||
tokens['#'] = true;
|
||||
++index;
|
||||
}
|
||||
|
||||
if('0' === spec.charAt(index)) {
|
||||
tokens['0'] = true;
|
||||
++index;
|
||||
}
|
||||
|
||||
match = SpecRegExp.Width.exec(spec.slice(index));
|
||||
tokens.width = match[0];
|
||||
incIndexByMatch();
|
||||
|
||||
if(',' === spec.charAt(index)) {
|
||||
tokens[','] = true;
|
||||
++index;
|
||||
}
|
||||
|
||||
if('.' === spec.charAt(index)) {
|
||||
++index;
|
||||
|
||||
match = SpecRegExp.Precision.exec(spec.slice(index));
|
||||
if(!match) {
|
||||
throw new ValueError('Format specifier missing precision');
|
||||
}
|
||||
|
||||
tokens.precision = match[0];
|
||||
incIndexByMatch();
|
||||
}
|
||||
|
||||
if(index < spec.length) {
|
||||
tokens.type = spec.charAt(index);
|
||||
++index;
|
||||
}
|
||||
|
||||
if(index < spec.length) {
|
||||
throw new ValueError('Invalid conversion specification');
|
||||
}
|
||||
|
||||
if(tokens[','] && 's' === tokens.type) {
|
||||
throw new ValueError(`Cannot specify ',' with 's'`); // eslint-disable-line quotes
|
||||
}
|
||||
|
||||
return tokens;
|
||||
}
|
||||
|
||||
function quote(s) {
|
||||
return `"${s.replace(/"/g, '\\"')}"`;
|
||||
}
|
||||
|
||||
function getPadAlign(align) {
|
||||
return {
|
||||
'<' : 'right',
|
||||
'>' : 'left',
|
||||
'^' : 'center',
|
||||
}[align] || '<';
|
||||
}
|
||||
|
||||
function formatString(value, tokens) {
|
||||
const fill = tokens.fill || (tokens['0'] ? '0' : ' ');
|
||||
const align = tokens.align || (tokens['0'] ? '=' : '<');
|
||||
const precision = Number(tokens.precision || value.length); // :TODO: consider pipe/ANSI length
|
||||
|
||||
if('' !== tokens.type && 's' !== tokens.type) {
|
||||
throw new ValueError(`Unknown format code "${tokens.type}" for String object`);
|
||||
}
|
||||
|
||||
if(tokens[',']) {
|
||||
throw new ValueError(`Cannot specify ',' with 's'`); // eslint-disable-line quotes
|
||||
}
|
||||
|
||||
if(tokens.sign) {
|
||||
throw new ValueError('Sign not allowed in string format specifier');
|
||||
}
|
||||
|
||||
if(tokens['#']) {
|
||||
throw new ValueError('Alternate form (#) not allowed in string format specifier');
|
||||
}
|
||||
|
||||
if('=' === align) {
|
||||
throw new ValueError('"=" alignment not allowed in string format specifier');
|
||||
}
|
||||
|
||||
return pad(value.slice(0, precision), parseInt(tokens.width), fill, getPadAlign(align));
|
||||
}
|
||||
|
||||
const FormatNumRegExp = {
|
||||
UpperType : /[A-Z]/,
|
||||
ExponentRep : /e[+-](?=\d$)/,
|
||||
};
|
||||
|
||||
function formatNumberHelper(n, precision, type) {
|
||||
if(FormatNumRegExp.UpperType.test(type)) {
|
||||
return formatNumberHelper(n, precision, type.toLowerCase()).toUpperCase();
|
||||
}
|
||||
|
||||
switch(type) {
|
||||
case 'c' : return String.fromCharCode(n);
|
||||
case 'd' : return n.toString(10);
|
||||
case 'b' : return n.toString(2);
|
||||
case 'o' : return n.toString(8);
|
||||
case 'x' : return n.toString(16);
|
||||
case 'e' : return n.toExponential(precision).replace(FormatNumRegExp.ExponentRep, '$&0');
|
||||
case 'f' : return n.toFixed(precision);
|
||||
case 'g' : return n.toPrecision(precision || 1);
|
||||
case '%' : return formatNumberHelper(n * 100, precision, 'f') + '%';
|
||||
case '' : return formatNumberHelper(n, precision, 'd');
|
||||
|
||||
default :
|
||||
throw new ValueError(`Unknown format code "${type}" for object of type 'float'`);
|
||||
}
|
||||
}
|
||||
|
||||
function formatNumber(value, tokens) {
|
||||
const fill = tokens.fill || (tokens['0'] ? '0' : ' ');
|
||||
const align = tokens.align || (tokens['0'] ? '=' : '>');
|
||||
const width = Number(tokens.width);
|
||||
const type = tokens.type || (tokens.precision ? 'g' : '');
|
||||
|
||||
if( [ 'c', 'd', 'b', 'o', 'x', 'X' ].indexOf(type) > -1) {
|
||||
if(0 !== value % 1) {
|
||||
throw new ValueError(`Cannot format non-integer with format specifier "${type}"`);
|
||||
}
|
||||
|
||||
if('' !== tokens.sign && 'c' !== type) {
|
||||
throw new ValueError(`Sign not allowed with integer format specifier 'c'`); // eslint-disable-line quotes
|
||||
}
|
||||
|
||||
if(tokens[','] && 'd' !== type) {
|
||||
throw new ValueError(`Cannot specify ',' with '${type}'`);
|
||||
}
|
||||
|
||||
if('' !== tokens.precision) {
|
||||
throw new ValueError('Precision not allowed in integer format specifier');
|
||||
}
|
||||
} else if( [ 'e', 'E', 'f', 'F', 'g', 'G', '%' ].indexOf(type) > - 1) {
|
||||
if(tokens['#']) {
|
||||
throw new ValueError('Alternate form (#) not allowed in float format specifier');
|
||||
}
|
||||
}
|
||||
|
||||
const s = formatNumberHelper(Math.abs(value), Number(tokens.precision || 6), type);
|
||||
const sign = value < 0 || 1 / value < 0 ?
|
||||
'-' :
|
||||
'-' === tokens.sign ? '' : tokens.sign;
|
||||
|
||||
const prefix = tokens['#'] && ( [ 'b', 'o', 'x', 'X' ].indexOf(type) > -1 ) ? '0' + type : '';
|
||||
|
||||
if(tokens[',']) {
|
||||
const match = /^(\d*)(.*)$/.exec(s);
|
||||
const separated = match[1].replace(/.(?=(...)+$)/g, '$&,') + match[2];
|
||||
|
||||
if('=' !== align) {
|
||||
return pad(sign + separated, width, fill, getPadAlign(align));
|
||||
}
|
||||
|
||||
if('0' === fill) {
|
||||
const shortfall = Math.max(0, width - sign.length - separated.length);
|
||||
const digits = /^\d*/.exec(separated)[0].length;
|
||||
let padding = '';
|
||||
// :TODO: do this differntly...
|
||||
for(let n = 0; n < shortfall; n++) {
|
||||
padding = ((digits + n) % 4 === 3 ? ',' : '0') + padding;
|
||||
}
|
||||
|
||||
return sign + (/^,/.test(padding) ? '0' : '') + padding + separated;
|
||||
}
|
||||
|
||||
return sign + pad(separated, width - sign.length, fill, getPadAlign('>'));
|
||||
}
|
||||
|
||||
if(0 === width) {
|
||||
return sign + prefix + s;
|
||||
}
|
||||
|
||||
if('=' === align) {
|
||||
return sign + prefix + pad(s, width - sign.length - prefix.length, fill, getPadAlign('>'));
|
||||
}
|
||||
|
||||
return pad(sign + prefix + s, width, fill, getPadAlign(align));
|
||||
}
|
||||
|
||||
const transformers = {
|
||||
// String standard
|
||||
toUpperCase : String.prototype.toUpperCase,
|
||||
toLowerCase : String.prototype.toLowerCase,
|
||||
|
||||
// some super l33b BBS styles!!
|
||||
styleUpper : (s) => stylizeString(s, 'upper'),
|
||||
styleLower : (s) => stylizeString(s, 'lower'),
|
||||
styleTitle : (s) => stylizeString(s, 'title'),
|
||||
styleFirstLower : (s) => stylizeString(s, 'first lower'),
|
||||
styleSmallVowels : (s) => stylizeString(s, 'small vowels'),
|
||||
styleBigVowels : (s) => stylizeString(s, 'big vowels'),
|
||||
styleSmallI : (s) => stylizeString(s, 'small i'),
|
||||
styleMixed : (s) => stylizeString(s, 'mixed'),
|
||||
styleL33t : (s) => stylizeString(s, 'l33t'),
|
||||
};
|
||||
|
||||
function transformValue(transformerName, value) {
|
||||
if(transformerName in transformers) {
|
||||
const transformer = transformers[transformerName];
|
||||
value = transformer.apply(value, [ value ] );
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
// :TODO: Use explicit set of chars for paths & function/transforms such that } is allowed as fill/etc.
|
||||
const REGEXP_BASIC_FORMAT = /{([^.!:}]+(?:\.[^.!:}]+)*)(?:\!([^:}]+))?(?:\:([^}]+))?}/g;
|
||||
|
||||
function getValue(obj, path) {
|
||||
const value = _.get(obj, path);
|
||||
if(value) {
|
||||
return _.isFunction(value) ? value() : value;
|
||||
}
|
||||
|
||||
throw new KeyError(quote(path));
|
||||
}
|
||||
|
||||
module.exports = function format(fmt, obj) {
|
||||
|
||||
const re = REGEXP_BASIC_FORMAT;
|
||||
let match;
|
||||
let pos;
|
||||
let out = '';
|
||||
do {
|
||||
pos = re.lastIndex;
|
||||
match = re.exec(fmt);
|
||||
|
||||
if(match) {
|
||||
if(match.index > pos) {
|
||||
out += fmt.slice(pos, match.index);
|
||||
}
|
||||
|
||||
const objPath = match[1];
|
||||
const transformer = match[2];
|
||||
const formatSpec = match[3];
|
||||
|
||||
let value = getValue(obj, objPath);
|
||||
if(transformer) {
|
||||
value = transformValue(transformer, value);
|
||||
}
|
||||
|
||||
const tokens = tokenizeFormatSpec(formatSpec || '');
|
||||
|
||||
if(!isNaN(parseInt(value))) {
|
||||
out += formatNumber(value, tokens);
|
||||
} else {
|
||||
out += formatString(value, tokens);
|
||||
}
|
||||
}
|
||||
|
||||
} while(0 !== re.lastIndex);
|
||||
|
||||
// remainder
|
||||
if(pos < fmt.length) {
|
||||
out += fmt.slice(pos);
|
||||
}
|
||||
|
||||
return out;
|
||||
};
|
Loading…
Reference in New Issue