UNPKG

6.11 kBJavaScriptView Raw
1"use strict";
2
3/**
4* Note that this module is exclude from istanbul code coverage
5* TODO : complete tests (see tests/sprintf.js) for full coverage
6*/
7
8const kindOf = require('mout/lang/kindOf');
9const repeat = require('../string/repeat');
10
11var re = {
12 not_string : /[^s]/,
13 number : /[diefg]/,
14 json : /[j]/,
15 not_json : /[^j]/,
16 text : /^[^\x25]+/,
17 modulo : /^\x25{2}/,
18 placeholder : /^\x25(?:([1-9]\d*)\$|\(([^)]+)\))?(\+)?(0|'[^$])?(-)?(\d+)?(?:\.(\d+))?([b-gijosuxX])/,
19 key : /^([a-z_][a-z_\d]*)/i,
20 key_access : /^\.([a-z_][a-z_\d]*)/i,
21 index_access : /^\[(\d+)\]/,
22 sign : /^[+-]/
23};
24
25function sprintf() {
26 var key = arguments[0];
27 var cache = sprintf.cache;
28 if(!(cache[key] && cache.hasOwnProperty(key)))
29 cache[key] = sprintf.parse(key);
30 return sprintf.format.call(null, cache[key], arguments);
31}
32
33/* istanbul ignore next */
34sprintf.format = function(parse_tree, argv) {
35 var cursor = 1;
36 var tree_length = parse_tree.length;
37 var node_type = "";
38 var output = [];
39 var is_positive = true;
40 var sign = "";
41
42 var arg;
43 var i;
44 var k;
45 var match;
46 var pad;
47 var pad_character;
48 var pad_length;
49
50 for(i = 0; i < tree_length; i++) {
51 node_type = kindOf(parse_tree[i]);
52 if(node_type === "String") {
53 output[output.length] = parse_tree[i];
54 } else if(node_type === "Array") {
55 match = parse_tree[i]; // convenience purposes only
56 if(match[2]) { // keyword argument
57 arg = argv[cursor];
58 for(k = 0; k < match[2].length; k++) {
59 if(!arg.hasOwnProperty(match[2][k]))
60 throw new Error(sprintf("[sprintf] property '%s' does not exist", match[2][k]));
61 arg = arg[match[2][k]];
62 }
63 } else if(match[1]) { // positional argument (explicit)
64 arg = argv[match[1]];
65 } else { // positional argument (implicit)
66 arg = argv[cursor++];
67 }
68
69 if(kindOf(arg) == "Function")
70 arg = arg();
71
72 if(re.not_string.test(match[8]) && re.not_json.test(match[8]) && (kindOf(arg) != "Number" && isNaN(arg)))
73 throw new TypeError(sprintf("[sprintf] expecting number but found %s", kindOf(arg)));
74
75 if(re.number.test(match[8]))
76 is_positive = arg >= 0;
77
78 switch(match[8]) {
79 case "b":
80 arg = arg.toString(2);
81 break;
82 case "c":
83 arg = String.fromCharCode(arg);
84 break;
85 case "d":
86 case "i":
87 arg = parseInt(arg, 10);
88 break;
89 case "j":
90 arg = JSON.stringify(arg, null, match[6] ? parseInt(match[6]) : 0);
91 break;
92 case "e":
93 arg = match[7] ? arg.toExponential(match[7]) : arg.toExponential();
94 break;
95 case "f":
96 arg = match[7] ? parseFloat(arg).toFixed(match[7]) : parseFloat(arg);
97 break;
98 case "g":
99 arg = match[7] ? parseFloat(arg).toPrecision(match[7]) : parseFloat(arg);
100 break;
101 case "o":
102 arg = arg.toString(8);
103 break;
104 case "s":
105 arg = ((arg = String(arg)) && match[7] ? arg.substring(0, match[7]) : arg);
106 break;
107 case "u":
108 arg = arg >>> 0;
109 break;
110 case "x":
111 arg = arg.toString(16);
112 break;
113 case "X":
114 arg = arg.toString(16).toUpperCase();
115 break;
116 }
117 if(re.json.test(match[8])) {
118 output[output.length] = arg;
119 } else {
120 if(re.number.test(match[8]) && (!is_positive || match[3])) {
121 sign = is_positive ? "+" : "-";
122 arg = arg.toString().replace(re.sign, "");
123 } else {
124 sign = "";
125 }
126 pad_character = match[4] ? match[4] === "0" ? "0" : match[4].charAt(1) : " ";
127 pad_length = match[6] - (sign + arg).length;
128 pad = match[6] ? (pad_length > 0 ? repeat(pad_character, pad_length) : "") : "";
129 output[output.length] = match[5] ? sign + arg + pad : (pad_character === "0" ? sign + pad + arg : pad + sign + arg);
130 }
131 }
132 }
133 return output.join("");
134};
135
136sprintf.cache = {};
137
138/* istanbul ignore next */
139sprintf.parse = function(fmt) {
140 var _fmt = fmt;
141 var match = [];
142 var parse_tree = [];
143 var arg_names = 0;
144 while(_fmt) {
145 if((match = re.text.exec(_fmt)) !== null) {
146 parse_tree[parse_tree.length] = match[0];
147 } else if((match = re.modulo.exec(_fmt)) !== null) {
148 parse_tree[parse_tree.length] = "%";
149 } else if((match = re.placeholder.exec(_fmt)) !== null) {
150 if(match[2]) {
151 arg_names |= 1;
152 var field_list = [];
153 var replacement_field = match[2];
154 var field_match = [];
155 if((field_match = re.key.exec(replacement_field)) !== null) {
156 field_list[field_list.length] = field_match[1];
157 while((replacement_field = replacement_field.substring(field_match[0].length)) !== "") {
158 if((field_match = re.key_access.exec(replacement_field)) !== null)
159 field_list[field_list.length] = field_match[1];
160 else if((field_match = re.index_access.exec(replacement_field)) !== null)
161 field_list[field_list.length] = field_match[1];
162 else
163 throw new SyntaxError("[sprintf] failed to parse named argument key");
164 }
165 } else {
166 throw new SyntaxError("[sprintf] failed to parse named argument key");
167 }
168 match[2] = field_list;
169 } else {
170 arg_names |= 2;
171 }
172
173 if(arg_names === 3)
174 throw new Error("[sprintf] mixing positional and named placeholders is not (yet) supported");
175
176 parse_tree[parse_tree.length] = match;
177 } else {
178 throw new SyntaxError("[sprintf] unexpected placeholder");
179 }
180 _fmt = _fmt.substring(match[0].length);
181 }
182 return parse_tree;
183};
184
185/* istanbul ignore next */
186var vsprintf = function(fmt, argv, _argv) {
187 _argv = (argv || []).slice(0);
188 _argv.splice(0, 0, fmt);
189 return sprintf.apply(null, _argv);
190};
191
192module.exports = sprintf;
193module.exports.vsprintf = vsprintf;