UNPKG

13.8 kBJavaScriptView Raw
1
2var util = require('util');
3
4var tokenize = function(/*String*/ str, /*RegExp*/ re, /*Function?*/ parseDelim, /*Object?*/ instance){
5 // summary:
6 // Split a string by a regular expression with the ability to capture the delimeters
7 // parseDelim:
8 // Each group (excluding the 0 group) is passed as a parameter. If the function returns
9 // a value, it's added to the list of tokens.
10 // instance:
11 // Used as the "this' instance when calling parseDelim
12 var tokens = [];
13 var match, content, lastIndex = 0;
14 while(match = re.exec(str)){
15 content = str.slice(lastIndex, re.lastIndex - match[0].length);
16 if(content.length){
17 tokens.push(content);
18 }
19 if(parseDelim){
20 var parsed = parseDelim.apply(instance, match.slice(1).concat(tokens.length));
21 if(typeof parsed != 'undefined'){
22 if(parsed.specifier === '%'){
23 tokens.push('%');
24 }else{
25 tokens.push(parsed);
26 }
27 }
28 }
29 lastIndex = re.lastIndex;
30 }
31 content = str.slice(lastIndex);
32 if(content.length){
33 tokens.push(content);
34 }
35 return tokens;
36}
37
38var Formatter = function(/*String*/ format){
39 var tokens = [];
40 this._mapped = false;
41 this._format = format;
42 this._tokens = tokenize(format, this._re, this._parseDelim, this);
43}
44
45Formatter.prototype._re = /\%(?:\(([\w_]+)\)|([1-9]\d*)\$)?([0 +\-\#]*)(\*|\d+)?(\.)?(\*|\d+)?[hlL]?([\%bscdeEfFgGioOuxX])/g;
46Formatter.prototype._parseDelim = function(mapping, intmapping, flags, minWidth, period, precision, specifier){
47 if(mapping){
48 this._mapped = true;
49 }
50 return {
51 mapping: mapping,
52 intmapping: intmapping,
53 flags: flags,
54 _minWidth: minWidth, // May be dependent on parameters
55 period: period,
56 _precision: precision, // May be dependent on parameters
57 specifier: specifier
58 };
59};
60Formatter.prototype._specifiers = {
61 b: {
62 base: 2,
63 isInt: true
64 },
65 o: {
66 base: 8,
67 isInt: true
68 },
69 x: {
70 base: 16,
71 isInt: true
72 },
73 X: {
74 extend: ['x'],
75 toUpper: true
76 },
77 d: {
78 base: 10,
79 isInt: true
80 },
81 i: {
82 extend: ['d']
83 },
84 u: {
85 extend: ['d'],
86 isUnsigned: true
87 },
88 c: {
89 setArg: function(token){
90 if(!isNaN(token.arg)){
91 var num = parseInt(token.arg);
92 if(num < 0 || num > 127){
93 throw new Error('invalid character code passed to %c in printf');
94 }
95 token.arg = isNaN(num) ? '' + num : String.fromCharCode(num);
96 }
97 }
98 },
99 s: {
100 setMaxWidth: function(token){
101 token.maxWidth = (token.period == '.') ? token.precision : -1;
102 }
103 },
104 e: {
105 isDouble: true,
106 doubleNotation: 'e'
107 },
108 E: {
109 extend: ['e'],
110 toUpper: true
111 },
112 f: {
113 isDouble: true,
114 doubleNotation: 'f'
115 },
116 F: {
117 extend: ['f']
118 },
119 g: {
120 isDouble: true,
121 doubleNotation: 'g'
122 },
123 G: {
124 extend: ['g'],
125 toUpper: true
126 },
127 O: {
128 isObject: true
129 },
130};
131Formatter.prototype.format = function(/*mixed...*/ filler){
132 if(this._mapped && typeof filler != 'object'){
133 throw new Error('format requires a mapping');
134 }
135
136 var str = '';
137 var position = 0;
138 for(var i = 0, token; i < this._tokens.length; i++){
139 token = this._tokens[i];
140
141 if(typeof token == 'string'){
142 str += token;
143 }else{
144 if(this._mapped){
145 if(typeof filler[token.mapping] == 'undefined'){
146 throw new Error('missing key ' + token.mapping);
147 }
148 token.arg = filler[token.mapping];
149 }else{
150 if(token.intmapping){
151 position = parseInt(token.intmapping) - 1;
152 }
153 if(position >= arguments.length){
154 throw new Error('got ' + arguments.length + ' printf arguments, insufficient for \'' + this._format + '\'');
155 }
156 token.arg = arguments[position++];
157 }
158
159 if(!token.compiled){
160 token.compiled = true;
161 token.sign = '';
162 token.zeroPad = false;
163 token.rightJustify = false;
164 token.alternative = false;
165
166 var flags = {};
167 for(var fi = token.flags.length; fi--;){
168 var flag = token.flags.charAt(fi);
169 flags[flag] = true;
170 switch(flag){
171 case ' ':
172 token.sign = ' ';
173 break;
174 case '+':
175 token.sign = '+';
176 break;
177 case '0':
178 token.zeroPad = (flags['-']) ? false : true;
179 break;
180 case '-':
181 token.rightJustify = true;
182 token.zeroPad = false;
183 break;
184 case '#':
185 token.alternative = true;
186 break;
187 default:
188 throw Error('bad formatting flag \'' + token.flags.charAt(fi) + '\'');
189 }
190 }
191
192 token.minWidth = (token._minWidth) ? parseInt(token._minWidth) : 0;
193 token.maxWidth = -1;
194 token.toUpper = false;
195 token.isUnsigned = false;
196 token.isInt = false;
197 token.isDouble = false;
198 token.isObject = false;
199 token.precision = 1;
200 if(token.period == '.'){
201 if(token._precision){
202 token.precision = parseInt(token._precision);
203 }else{
204 token.precision = 0;
205 }
206 }
207
208 var mixins = this._specifiers[token.specifier];
209 if(typeof mixins == 'undefined'){
210 throw new Error('unexpected specifier \'' + token.specifier + '\'');
211 }
212 if(mixins.extend){
213 var s = this._specifiers[mixins.extend];
214 for(var k in s){
215 mixins[k] = s[k]
216 }
217 delete mixins.extend;
218 }
219 for(var l in mixins){
220 token[l] = mixins[l];
221 }
222 }
223
224 if(typeof token.setArg == 'function'){
225 token.setArg(token);
226 }
227
228 if(typeof token.setMaxWidth == 'function'){
229 token.setMaxWidth(token);
230 }
231
232 if(token._minWidth == '*'){
233 if(this._mapped){
234 throw new Error('* width not supported in mapped formats');
235 }
236 token.minWidth = parseInt(arguments[position++]);
237 if(isNaN(token.minWidth)){
238 throw new Error('the argument for * width at position ' + position + ' is not a number in ' + this._format);
239 }
240 // negative width means rightJustify
241 if (token.minWidth < 0) {
242 token.rightJustify = true;
243 token.minWidth = -token.minWidth;
244 }
245 }
246
247 if(token._precision == '*' && token.period == '.'){
248 if(this._mapped){
249 throw new Error('* precision not supported in mapped formats');
250 }
251 token.precision = parseInt(arguments[position++]);
252 if(isNaN(token.precision)){
253 throw Error('the argument for * precision at position ' + position + ' is not a number in ' + this._format);
254 }
255 // negative precision means unspecified
256 if (token.precision < 0) {
257 token.precision = 1;
258 token.period = '';
259 }
260 }
261 if(token.isInt){
262 // a specified precision means no zero padding
263 if(token.period == '.'){
264 token.zeroPad = false;
265 }
266 this.formatInt(token);
267 }else if(token.isDouble){
268 if(token.period != '.'){
269 token.precision = 6;
270 }
271 this.formatDouble(token);
272 }else if(token.isObject){
273 this.formatObject(token);
274 }
275 this.fitField(token);
276
277 str += '' + token.arg;
278 }
279 }
280
281 return str;
282};
283Formatter.prototype._zeros10 = '0000000000';
284Formatter.prototype._spaces10 = ' ';
285Formatter.prototype.formatInt = function(token) {
286 var i = parseInt(token.arg);
287 if(!isFinite(i)){ // isNaN(f) || f == Number.POSITIVE_INFINITY || f == Number.NEGATIVE_INFINITY)
288 // allow this only if arg is number
289 if(typeof token.arg != 'number'){
290 throw new Error('format argument \'' + token.arg + '\' not an integer; parseInt returned ' + i);
291 }
292 //return '' + i;
293 i = 0;
294 }
295
296 // if not base 10, make negatives be positive
297 // otherwise, (-10).toString(16) is '-a' instead of 'fffffff6'
298 if(i < 0 && (token.isUnsigned || token.base != 10)){
299 i = 0xffffffff + i + 1;
300 }
301
302 if(i < 0){
303 token.arg = (- i).toString(token.base);
304 this.zeroPad(token);
305 token.arg = '-' + token.arg;
306 }else{
307 token.arg = i.toString(token.base);
308 // need to make sure that argument 0 with precision==0 is formatted as ''
309 if(!i && !token.precision){
310 token.arg = '';
311 }else{
312 this.zeroPad(token);
313 }
314 if(token.sign){
315 token.arg = token.sign + token.arg;
316 }
317 }
318 if(token.base == 16){
319 if(token.alternative){
320 token.arg = '0x' + token.arg;
321 }
322 token.arg = token.toUpper ? token.arg.toUpperCase() : token.arg.toLowerCase();
323 }
324 if(token.base == 8){
325 if(token.alternative && token.arg.charAt(0) != '0'){
326 token.arg = '0' + token.arg;
327 }
328 }
329};
330Formatter.prototype.formatDouble = function(token) {
331 var f = parseFloat(token.arg);
332 if(!isFinite(f)){ // isNaN(f) || f == Number.POSITIVE_INFINITY || f == Number.NEGATIVE_INFINITY)
333 // allow this only if arg is number
334 if(typeof token.arg != 'number'){
335 throw new Error('format argument \'' + token.arg + '\' not a float; parseFloat returned ' + f);
336 }
337 // C99 says that for 'f':
338 // infinity -> '[-]inf' or '[-]infinity' ('[-]INF' or '[-]INFINITY' for 'F')
339 // NaN -> a string starting with 'nan' ('NAN' for 'F')
340 // this is not commonly implemented though.
341 //return '' + f;
342 f = 0;
343 }
344
345 switch(token.doubleNotation) {
346 case 'e': {
347 token.arg = f.toExponential(token.precision);
348 break;
349 }
350 case 'f': {
351 token.arg = f.toFixed(token.precision);
352 break;
353 }
354 case 'g': {
355 // C says use 'e' notation if exponent is < -4 or is >= prec
356 // ECMAScript for toPrecision says use exponential notation if exponent is >= prec,
357 // though step 17 of toPrecision indicates a test for < -6 to force exponential.
358 if(Math.abs(f) < 0.0001){
359 //print('forcing exponential notation for f=' + f);
360 token.arg = f.toExponential(token.precision > 0 ? token.precision - 1 : token.precision);
361 }else{
362 token.arg = f.toPrecision(token.precision);
363 }
364
365 // In C, unlike 'f', 'gG' removes trailing 0s from fractional part, unless alternative format flag ('#').
366 // But ECMAScript formats toPrecision as 0.00100000. So remove trailing 0s.
367 if(!token.alternative){
368 //print('replacing trailing 0 in \'' + s + '\'');
369 token.arg = token.arg.replace(/(\..*[^0])0*e/, '$1e');
370 // if fractional part is entirely 0, remove it and decimal point
371 token.arg = token.arg.replace(/\.0*e/, 'e').replace(/\.0$/,'');
372 }
373 break;
374 }
375 default: throw new Error('unexpected double notation \'' + token.doubleNotation + '\'');
376 }
377
378 // C says that exponent must have at least two digits.
379 // But ECMAScript does not; toExponential results in things like '1.000000e-8' and '1.000000e+8'.
380 // Note that s.replace(/e([\+\-])(\d)/, 'e$10$2') won't work because of the '$10' instead of '$1'.
381 // And replace(re, func) isn't supported on IE50 or Safari1.
382 token.arg = token.arg.replace(/e\+(\d)$/, 'e+0$1').replace(/e\-(\d)$/, 'e-0$1');
383
384 // if alt, ensure a decimal point
385 if(token.alternative){
386 token.arg = token.arg.replace(/^(\d+)$/,'$1.');
387 token.arg = token.arg.replace(/^(\d+)e/,'$1.e');
388 }
389
390 if(f >= 0 && token.sign){
391 token.arg = token.sign + token.arg;
392 }
393
394 token.arg = token.toUpper ? token.arg.toUpperCase() : token.arg.toLowerCase();
395};
396Formatter.prototype.formatObject = function(token) {
397 // If no precision is specified, then reset it to null (infinite depth).
398 var precision = (token.period === '.') ? token.precision : null;
399 token.arg = util.inspect(token.arg, !token.alternative, precision);
400};
401Formatter.prototype.zeroPad = function(token, /*Int*/ length) {
402 length = (arguments.length == 2) ? length : token.precision;
403 var negative = false;
404 if(typeof token.arg != "string"){
405 token.arg = "" + token.arg;
406 }
407 if (token.arg.substr(0,1) === '-') {
408 negative = true;
409 token.arg = token.arg.substr(1);
410 }
411
412 var tenless = length - 10;
413 while(token.arg.length < tenless){
414 token.arg = (token.rightJustify) ? token.arg + this._zeros10 : this._zeros10 + token.arg;
415 }
416 var pad = length - token.arg.length;
417 token.arg = (token.rightJustify) ? token.arg + this._zeros10.substring(0, pad) : this._zeros10.substring(0, pad) + token.arg;
418 if (negative) token.arg = '-' + token.arg;
419};
420Formatter.prototype.fitField = function(token) {
421 if(token.maxWidth >= 0 && token.arg.length > token.maxWidth){
422 return token.arg.substring(0, token.maxWidth);
423 }
424 if(token.zeroPad){
425 this.zeroPad(token, token.minWidth);
426 return;
427 }
428 this.spacePad(token);
429};
430Formatter.prototype.spacePad = function(token, /*Int*/ length) {
431 length = (arguments.length == 2) ? length : token.minWidth;
432 if(typeof token.arg != 'string'){
433 token.arg = '' + token.arg;
434 }
435 var tenless = length - 10;
436 while(token.arg.length < tenless){
437 token.arg = (token.rightJustify) ? token.arg + this._spaces10 : this._spaces10 + token.arg;
438 }
439 var pad = length - token.arg.length;
440 token.arg = (token.rightJustify) ? token.arg + this._spaces10.substring(0, pad) : this._spaces10.substring(0, pad) + token.arg;
441};
442
443
444module.exports = function(){
445 var args = Array.prototype.slice.call(arguments),
446 stream, format;
447 if(args[0] instanceof require('stream').Stream){
448 stream = args.shift();
449 }
450 format = args.shift();
451 var formatter = new Formatter(format);
452 var string = formatter.format.apply(formatter, args);
453 if(stream){
454 stream.write(string);
455 }else{
456 return string;
457 }
458};
459
460module.exports.Formatter = Formatter;
461