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