should-format.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528
  1. import t from 'should-type';
  2. import { forEach } from 'should-type-adaptors';
  3. function looksLikeANumber(n) {
  4. return !!n.match(/\d+/);
  5. }
  6. function keyCompare(a, b) {
  7. var aNum = looksLikeANumber(a);
  8. var bNum = looksLikeANumber(b);
  9. if (aNum && bNum) {
  10. return 1*a - 1*b;
  11. } else if (aNum && !bNum) {
  12. return -1;
  13. } else if (!aNum && bNum) {
  14. return 1;
  15. } else {
  16. return a.localeCompare(b);
  17. }
  18. }
  19. function genKeysFunc(f) {
  20. return function(value) {
  21. var k = f(value);
  22. k.sort(keyCompare);
  23. return k;
  24. };
  25. }
  26. function Formatter(opts) {
  27. opts = opts || {};
  28. this.seen = [];
  29. var keysFunc;
  30. if (typeof opts.keysFunc === 'function') {
  31. keysFunc = opts.keysFunc;
  32. } else if (opts.keys === false) {
  33. keysFunc = Object.getOwnPropertyNames;
  34. } else {
  35. keysFunc = Object.keys;
  36. }
  37. this.getKeys = genKeysFunc(keysFunc);
  38. this.maxLineLength = typeof opts.maxLineLength === 'number' ? opts.maxLineLength : 60;
  39. this.propSep = opts.propSep || ',';
  40. this.isUTCdate = !!opts.isUTCdate;
  41. }
  42. Formatter.prototype = {
  43. constructor: Formatter,
  44. format: function(value) {
  45. var tp = t(value);
  46. if (this.alreadySeen(value)) {
  47. return '[Circular]';
  48. }
  49. var tries = tp.toTryTypes();
  50. var f = this.defaultFormat;
  51. while (tries.length) {
  52. var toTry = tries.shift();
  53. var name = Formatter.formatterFunctionName(toTry);
  54. if (this[name]) {
  55. f = this[name];
  56. break;
  57. }
  58. }
  59. return f.call(this, value).trim();
  60. },
  61. defaultFormat: function(obj) {
  62. return String(obj);
  63. },
  64. alreadySeen: function(value) {
  65. return this.seen.indexOf(value) >= 0;
  66. }
  67. };
  68. Formatter.addType = function addType(tp, f) {
  69. Formatter.prototype[Formatter.formatterFunctionName(tp)] = f;
  70. };
  71. Formatter.formatterFunctionName = function formatterFunctionName(tp) {
  72. return '_format_' + tp.toString('_');
  73. };
  74. var EOL = '\n';
  75. function indent(v, indentation) {
  76. return v
  77. .split(EOL)
  78. .map(function(vv) {
  79. return indentation + vv;
  80. })
  81. .join(EOL);
  82. }
  83. function pad(str, value, filler) {
  84. str = String(str);
  85. var isRight = false;
  86. if (value < 0) {
  87. isRight = true;
  88. value = -value;
  89. }
  90. if (str.length < value) {
  91. var padding = new Array(value - str.length + 1).join(filler);
  92. return isRight ? str + padding : padding + str;
  93. } else {
  94. return str;
  95. }
  96. }
  97. function pad0(str, value) {
  98. return pad(str, value, '0');
  99. }
  100. var functionNameRE = /^\s*function\s*(\S*)\s*\(/;
  101. function functionName(f) {
  102. if (f.name) {
  103. return f.name;
  104. }
  105. var matches = f.toString().match(functionNameRE);
  106. if (matches === null) {
  107. // `functionNameRE` doesn't match arrow functions.
  108. return '';
  109. }
  110. var name = matches[1];
  111. return name;
  112. }
  113. function constructorName(obj) {
  114. while (obj) {
  115. var descriptor = Object.getOwnPropertyDescriptor(obj, 'constructor');
  116. if (descriptor !== undefined && typeof descriptor.value === 'function') {
  117. var name = functionName(descriptor.value);
  118. if (name !== '') {
  119. return name;
  120. }
  121. }
  122. obj = Object.getPrototypeOf(obj);
  123. }
  124. }
  125. var INDENT = ' ';
  126. function addSpaces(str) {
  127. return indent(str, INDENT);
  128. }
  129. function typeAdaptorForEachFormat(obj, opts) {
  130. opts = opts || {};
  131. var filterKey = opts.filterKey || function() { return true; };
  132. var formatKey = opts.formatKey || this.format;
  133. var formatValue = opts.formatValue || this.format;
  134. var keyValueSep = typeof opts.keyValueSep !== 'undefined' ? opts.keyValueSep : ': ';
  135. this.seen.push(obj);
  136. var formatLength = 0;
  137. var pairs = [];
  138. forEach(obj, function(value, key) {
  139. if (!filterKey(key)) {
  140. return;
  141. }
  142. var formattedKey = formatKey.call(this, key);
  143. var formattedValue = formatValue.call(this, value, key);
  144. var pair = formattedKey ? (formattedKey + keyValueSep + formattedValue) : formattedValue;
  145. formatLength += pair.length;
  146. pairs.push(pair);
  147. }, this);
  148. this.seen.pop();
  149. (opts.additionalKeys || []).forEach(function(keyValue) {
  150. var pair = keyValue[0] + keyValueSep + this.format(keyValue[1]);
  151. formatLength += pair.length;
  152. pairs.push(pair);
  153. }, this);
  154. var prefix = opts.prefix || constructorName(obj) || '';
  155. if (prefix.length > 0) {
  156. prefix += ' ';
  157. }
  158. var lbracket, rbracket;
  159. if (Array.isArray(opts.brackets)) {
  160. lbracket = opts.brackets[0];
  161. rbracket = opts.brackets[1];
  162. } else {
  163. lbracket = '{';
  164. rbracket = '}';
  165. }
  166. var rootValue = opts.value || '';
  167. if (pairs.length === 0) {
  168. return rootValue || (prefix + lbracket + rbracket);
  169. }
  170. if (formatLength <= this.maxLineLength) {
  171. return prefix + lbracket + ' ' + (rootValue ? rootValue + ' ' : '') + pairs.join(this.propSep + ' ') + ' ' + rbracket;
  172. } else {
  173. return prefix + lbracket + '\n' + (rootValue ? ' ' + rootValue + '\n' : '') + pairs.map(addSpaces).join(this.propSep + '\n') + '\n' + rbracket;
  174. }
  175. }
  176. function formatPlainObjectKey(key) {
  177. return typeof key === 'string' && key.match(/^[a-zA-Z_$][a-zA-Z_$0-9]*$/) ? key : this.format(key);
  178. }
  179. function getPropertyDescriptor(obj, key) {
  180. var desc;
  181. try {
  182. desc = Object.getOwnPropertyDescriptor(obj, key) || { value: obj[key] };
  183. } catch (e) {
  184. desc = { value: e };
  185. }
  186. return desc;
  187. }
  188. function formatPlainObjectValue(obj, key) {
  189. var desc = getPropertyDescriptor(obj, key);
  190. if (desc.get && desc.set) {
  191. return '[Getter/Setter]';
  192. }
  193. if (desc.get) {
  194. return '[Getter]';
  195. }
  196. if (desc.set) {
  197. return '[Setter]';
  198. }
  199. return this.format(desc.value);
  200. }
  201. function formatPlainObject(obj, opts) {
  202. opts = opts || {};
  203. opts.keyValueSep = ': ';
  204. opts.formatKey = opts.formatKey || formatPlainObjectKey;
  205. opts.formatValue = opts.formatValue || function(value, key) {
  206. return formatPlainObjectValue.call(this, obj, key);
  207. };
  208. return typeAdaptorForEachFormat.call(this, obj, opts);
  209. }
  210. function formatWrapper1(value) {
  211. return formatPlainObject.call(this, value, {
  212. additionalKeys: [['[[PrimitiveValue]]', value.valueOf()]]
  213. });
  214. }
  215. function formatWrapper2(value) {
  216. var realValue = value.valueOf();
  217. return formatPlainObject.call(this, value, {
  218. filterKey: function(key) {
  219. //skip useless indexed properties
  220. return !(key.match(/\d+/) && parseInt(key, 10) < realValue.length);
  221. },
  222. additionalKeys: [['[[PrimitiveValue]]', realValue]]
  223. });
  224. }
  225. function formatRegExp(value) {
  226. return formatPlainObject.call(this, value, {
  227. value: String(value)
  228. });
  229. }
  230. function formatFunction(value) {
  231. return formatPlainObject.call(this, value, {
  232. prefix: 'Function',
  233. additionalKeys: [['name', functionName(value)]]
  234. });
  235. }
  236. function formatArray(value) {
  237. return formatPlainObject.call(this, value, {
  238. formatKey: function(key) {
  239. if (!key.match(/\d+/)) {
  240. return formatPlainObjectKey.call(this, key);
  241. }
  242. },
  243. brackets: ['[', ']']
  244. });
  245. }
  246. function formatArguments(value) {
  247. return formatPlainObject.call(this, value, {
  248. formatKey: function(key) {
  249. if (!key.match(/\d+/)) {
  250. return formatPlainObjectKey.call(this, key);
  251. }
  252. },
  253. brackets: ['[', ']'],
  254. prefix: 'Arguments'
  255. });
  256. }
  257. function _formatDate(value, isUTC) {
  258. var prefix = isUTC ? 'UTC' : '';
  259. var date = value['get' + prefix + 'FullYear']() +
  260. '-' +
  261. pad0(value['get' + prefix + 'Month']() + 1, 2) +
  262. '-' +
  263. pad0(value['get' + prefix + 'Date'](), 2);
  264. var time = pad0(value['get' + prefix + 'Hours'](), 2) +
  265. ':' +
  266. pad0(value['get' + prefix + 'Minutes'](), 2) +
  267. ':' +
  268. pad0(value['get' + prefix + 'Seconds'](), 2) +
  269. '.' +
  270. pad0(value['get' + prefix + 'Milliseconds'](), 3);
  271. var to = value.getTimezoneOffset();
  272. var absTo = Math.abs(to);
  273. var hours = Math.floor(absTo / 60);
  274. var minutes = absTo - hours * 60;
  275. var tzFormat = (to < 0 ? '+' : '-') + pad0(hours, 2) + pad0(minutes, 2);
  276. return date + ' ' + time + (isUTC ? '' : ' ' + tzFormat);
  277. }
  278. function formatDate(value) {
  279. return formatPlainObject.call(this, value, { value: _formatDate(value, this.isUTCdate) });
  280. }
  281. function formatError(value) {
  282. return formatPlainObject.call(this, value, {
  283. prefix: value.name,
  284. additionalKeys: [['message', value.message]]
  285. });
  286. }
  287. function generateFormatForNumberArray(lengthProp, name, padding) {
  288. return function(value) {
  289. var max = this.byteArrayMaxLength || 50;
  290. var length = value[lengthProp];
  291. var formattedValues = [];
  292. var len = 0;
  293. for (var i = 0; i < max && i < length; i++) {
  294. var b = value[i] || 0;
  295. var v = pad0(b.toString(16), padding);
  296. len += v.length;
  297. formattedValues.push(v);
  298. }
  299. var prefix = value.constructor.name || name || '';
  300. if (prefix) {
  301. prefix += ' ';
  302. }
  303. if (formattedValues.length === 0) {
  304. return prefix + '[]';
  305. }
  306. if (len <= this.maxLineLength) {
  307. return prefix + '[ ' + formattedValues.join(this.propSep + ' ') + ' ' + ']';
  308. } else {
  309. return prefix + '[\n' + formattedValues.map(addSpaces).join(this.propSep + '\n') + '\n' + ']';
  310. }
  311. };
  312. }
  313. function formatMap(obj) {
  314. return typeAdaptorForEachFormat.call(this, obj, {
  315. keyValueSep: ' => '
  316. });
  317. }
  318. function formatSet(obj) {
  319. return typeAdaptorForEachFormat.call(this, obj, {
  320. keyValueSep: '',
  321. formatKey: function() { return ''; }
  322. });
  323. }
  324. function genSimdVectorFormat(constructorName, length) {
  325. return function(value) {
  326. var Constructor = value.constructor;
  327. var extractLane = Constructor.extractLane;
  328. var len = 0;
  329. var props = [];
  330. for (var i = 0; i < length; i ++) {
  331. var key = this.format(extractLane(value, i));
  332. len += key.length;
  333. props.push(key);
  334. }
  335. if (len <= this.maxLineLength) {
  336. return constructorName + ' [ ' + props.join(this.propSep + ' ') + ' ]';
  337. } else {
  338. return constructorName + ' [\n' + props.map(addSpaces).join(this.propSep + '\n') + '\n' + ']';
  339. }
  340. };
  341. }
  342. function defaultFormat(value, opts) {
  343. return new Formatter(opts).format(value);
  344. }
  345. defaultFormat.Formatter = Formatter;
  346. defaultFormat.addSpaces = addSpaces;
  347. defaultFormat.pad0 = pad0;
  348. defaultFormat.functionName = functionName;
  349. defaultFormat.constructorName = constructorName;
  350. defaultFormat.formatPlainObjectKey = formatPlainObjectKey;
  351. defaultFormat.formatPlainObject = formatPlainObject;
  352. defaultFormat.typeAdaptorForEachFormat = typeAdaptorForEachFormat;
  353. // adding primitive types
  354. Formatter.addType(new t.Type(t.UNDEFINED), function() {
  355. return 'undefined';
  356. });
  357. Formatter.addType(new t.Type(t.NULL), function() {
  358. return 'null';
  359. });
  360. Formatter.addType(new t.Type(t.BOOLEAN), function(value) {
  361. return value ? 'true': 'false';
  362. });
  363. Formatter.addType(new t.Type(t.SYMBOL), function(value) {
  364. return value.toString();
  365. });
  366. Formatter.addType(new t.Type(t.NUMBER), function(value) {
  367. if (value === 0 && 1 / value < 0) {
  368. return '-0';
  369. }
  370. return String(value);
  371. });
  372. Formatter.addType(new t.Type(t.STRING), function(value) {
  373. return '\'' + JSON.stringify(value).replace(/^"|"$/g, '')
  374. .replace(/'/g, "\\'")
  375. .replace(/\\"/g, '"') + '\'';
  376. });
  377. Formatter.addType(new t.Type(t.FUNCTION), formatFunction);
  378. // plain object
  379. Formatter.addType(new t.Type(t.OBJECT), formatPlainObject);
  380. // type wrappers
  381. Formatter.addType(new t.Type(t.OBJECT, t.NUMBER), formatWrapper1);
  382. Formatter.addType(new t.Type(t.OBJECT, t.BOOLEAN), formatWrapper1);
  383. Formatter.addType(new t.Type(t.OBJECT, t.STRING), formatWrapper2);
  384. Formatter.addType(new t.Type(t.OBJECT, t.REGEXP), formatRegExp);
  385. Formatter.addType(new t.Type(t.OBJECT, t.ARRAY), formatArray);
  386. Formatter.addType(new t.Type(t.OBJECT, t.ARGUMENTS), formatArguments);
  387. Formatter.addType(new t.Type(t.OBJECT, t.DATE), formatDate);
  388. Formatter.addType(new t.Type(t.OBJECT, t.ERROR), formatError);
  389. Formatter.addType(new t.Type(t.OBJECT, t.SET), formatSet);
  390. Formatter.addType(new t.Type(t.OBJECT, t.MAP), formatMap);
  391. Formatter.addType(new t.Type(t.OBJECT, t.WEAK_MAP), formatMap);
  392. Formatter.addType(new t.Type(t.OBJECT, t.WEAK_SET), formatSet);
  393. Formatter.addType(new t.Type(t.OBJECT, t.BUFFER), generateFormatForNumberArray('length', 'Buffer', 2));
  394. Formatter.addType(new t.Type(t.OBJECT, t.ARRAY_BUFFER), generateFormatForNumberArray('byteLength', 'ArrayBuffer', 2));
  395. Formatter.addType(new t.Type(t.OBJECT, t.TYPED_ARRAY, 'int8'), generateFormatForNumberArray('length', 'Int8Array', 2));
  396. Formatter.addType(new t.Type(t.OBJECT, t.TYPED_ARRAY, 'uint8'), generateFormatForNumberArray('length', 'Uint8Array', 2));
  397. Formatter.addType(new t.Type(t.OBJECT, t.TYPED_ARRAY, 'uint8clamped'), generateFormatForNumberArray('length', 'Uint8ClampedArray', 2));
  398. Formatter.addType(new t.Type(t.OBJECT, t.TYPED_ARRAY, 'int16'), generateFormatForNumberArray('length', 'Int16Array', 4));
  399. Formatter.addType(new t.Type(t.OBJECT, t.TYPED_ARRAY, 'uint16'), generateFormatForNumberArray('length', 'Uint16Array', 4));
  400. Formatter.addType(new t.Type(t.OBJECT, t.TYPED_ARRAY, 'int32'), generateFormatForNumberArray('length', 'Int32Array', 8));
  401. Formatter.addType(new t.Type(t.OBJECT, t.TYPED_ARRAY, 'uint32'), generateFormatForNumberArray('length', 'Uint32Array', 8));
  402. Formatter.addType(new t.Type(t.OBJECT, t.SIMD, 'bool16x8'), genSimdVectorFormat('Bool16x8', 8));
  403. Formatter.addType(new t.Type(t.OBJECT, t.SIMD, 'bool32x4'), genSimdVectorFormat('Bool32x4', 4));
  404. Formatter.addType(new t.Type(t.OBJECT, t.SIMD, 'bool8x16'), genSimdVectorFormat('Bool8x16', 16));
  405. Formatter.addType(new t.Type(t.OBJECT, t.SIMD, 'float32x4'), genSimdVectorFormat('Float32x4', 4));
  406. Formatter.addType(new t.Type(t.OBJECT, t.SIMD, 'int16x8'), genSimdVectorFormat('Int16x8', 8));
  407. Formatter.addType(new t.Type(t.OBJECT, t.SIMD, 'int32x4'), genSimdVectorFormat('Int32x4', 4));
  408. Formatter.addType(new t.Type(t.OBJECT, t.SIMD, 'int8x16'), genSimdVectorFormat('Int8x16', 16));
  409. Formatter.addType(new t.Type(t.OBJECT, t.SIMD, 'uint16x8'), genSimdVectorFormat('Uint16x8', 8));
  410. Formatter.addType(new t.Type(t.OBJECT, t.SIMD, 'uint32x4'), genSimdVectorFormat('Uint32x4', 4));
  411. Formatter.addType(new t.Type(t.OBJECT, t.SIMD, 'uint8x16'), genSimdVectorFormat('Uint8x16', 16));
  412. Formatter.addType(new t.Type(t.OBJECT, t.PROMISE), function() {
  413. return '[Promise]';//TODO it could be nice to inspect its state and value
  414. });
  415. Formatter.addType(new t.Type(t.OBJECT, t.XHR), function() {
  416. return '[XMLHttpRequest]';//TODO it could be nice to inspect its state
  417. });
  418. Formatter.addType(new t.Type(t.OBJECT, t.HTML_ELEMENT), function(value) {
  419. return value.outerHTML;
  420. });
  421. Formatter.addType(new t.Type(t.OBJECT, t.HTML_ELEMENT, '#text'), function(value) {
  422. return value.nodeValue;
  423. });
  424. Formatter.addType(new t.Type(t.OBJECT, t.HTML_ELEMENT, '#document'), function(value) {
  425. return value.documentElement.outerHTML;
  426. });
  427. Formatter.addType(new t.Type(t.OBJECT, t.HOST), function() {
  428. return '[Host]';
  429. });
  430. export default defaultFormat;