Stringer.js 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153
  1. 'use strict';
  2. const {Transform} = require('stream');
  3. const noCommaAfter = {startObject: 1, startArray: 1, endKey: 1, keyValue: 1},
  4. noSpaceAfter = {endObject: 1, endArray: 1, '': 1},
  5. noSpaceBefore = {startObject: 1, startArray: 1},
  6. depthIncrement = {startObject: 1, startArray: 1},
  7. depthDecrement = {endObject: 1, endArray: 1},
  8. values = {startKey: 'keyValue', startString: 'stringValue', startNumber: 'numberValue'},
  9. stopNames = {startKey: 'endKey', startString: 'endString', startNumber: 'endNumber'},
  10. symbols = {
  11. startObject: '{',
  12. endObject: '}',
  13. startArray: '[',
  14. endArray: ']',
  15. startKey: '"',
  16. endKey: '":',
  17. startString: '"',
  18. endString: '"',
  19. startNumber: '',
  20. endNumber: '',
  21. nullValue: 'null',
  22. trueValue: 'true',
  23. falseValue: 'false'
  24. };
  25. const skipValue = endName =>
  26. function (chunk, encoding, callback) {
  27. if (chunk.name === endName) {
  28. this._transform = this._prev_transform;
  29. }
  30. callback(null);
  31. };
  32. const replaceSymbols = {'\b': '\\b', '\f': '\\f', '\n': '\\n', '\r': '\\r', '\t': '\\t', '"': '\\"', '\\': '\\\\'};
  33. const sanitizeString = value =>
  34. value.replace(/[\b\f\n\r\t\"\\\u0000-\u001F\u007F-\u009F]/g, match =>
  35. replaceSymbols.hasOwnProperty(match) ? replaceSymbols[match] : '\\u' + ('0000' + match.charCodeAt(0).toString(16)).slice(-4)
  36. );
  37. const doNothing = () => {};
  38. class Stringer extends Transform {
  39. static make(options) {
  40. return new Stringer(options);
  41. }
  42. constructor(options) {
  43. super(Object.assign({}, options, {writableObjectMode: true, readableObjectMode: false}));
  44. this._values = {};
  45. if (options) {
  46. 'useValues' in options && (this._values.keyValue = this._values.stringValue = this._values.numberValue = options.useValues);
  47. 'useKeyValues' in options && (this._values.keyValue = options.useKeyValues);
  48. 'useStringValues' in options && (this._values.stringValue = options.useStringValues);
  49. 'useNumberValues' in options && (this._values.numberValue = options.useNumberValues);
  50. this._makeArray = options.makeArray;
  51. }
  52. this._prev = '';
  53. this._depth = 0;
  54. if (this._makeArray) {
  55. this._transform = this._arrayTransform;
  56. this._flush = this._arrayFlush;
  57. }
  58. }
  59. _arrayTransform(chunk, encoding, callback) {
  60. // it runs once
  61. delete this._transform;
  62. this._transform({name: 'startArray'}, encoding, doNothing);
  63. this._transform(chunk, encoding, callback);
  64. }
  65. _arrayFlush(callback) {
  66. if (this._transform === this._arrayTransform) {
  67. delete this._transform;
  68. this._transform({name: 'startArray'}, null, doNothing);
  69. }
  70. this._transform({name: 'endArray'}, null, callback);
  71. }
  72. _transform(chunk, _, callback) {
  73. if (this._values[chunk.name]) {
  74. if (this._depth && noCommaAfter[this._prev] !== 1) this.push(',');
  75. switch (chunk.name) {
  76. case 'keyValue':
  77. this.push('"' + sanitizeString(chunk.value) + '":');
  78. break;
  79. case 'stringValue':
  80. this.push('"' + sanitizeString(chunk.value) + '"');
  81. break;
  82. case 'numberValue':
  83. this.push(chunk.value);
  84. break;
  85. }
  86. } else {
  87. // filter out values
  88. switch (chunk.name) {
  89. case 'endObject':
  90. case 'endArray':
  91. case 'endKey':
  92. case 'endString':
  93. case 'endNumber':
  94. this.push(symbols[chunk.name]);
  95. break;
  96. case 'stringChunk':
  97. this.push(sanitizeString(chunk.value));
  98. break;
  99. case 'numberChunk':
  100. this.push(chunk.value);
  101. break;
  102. case 'keyValue':
  103. case 'stringValue':
  104. case 'numberValue':
  105. // skip completely
  106. break;
  107. case 'startKey':
  108. case 'startString':
  109. case 'startNumber':
  110. if (this._values[values[chunk.name]]) {
  111. this._prev_transform = this._transform;
  112. this._transform = skipValue(stopNames[chunk.name]);
  113. return callback(null);
  114. }
  115. // intentional fall down
  116. default:
  117. // case 'startObject': case 'startArray': case 'startKey': case 'startString':
  118. // case 'startNumber': case 'nullValue': case 'trueValue': case 'falseValue':
  119. if (this._depth) {
  120. if (noCommaAfter[this._prev] !== 1) this.push(',');
  121. } else {
  122. if (noSpaceAfter[this._prev] !== 1 && noSpaceBefore[chunk.name] !== 1) this.push(' ');
  123. }
  124. this.push(symbols[chunk.name]);
  125. break;
  126. }
  127. if (depthIncrement[chunk.name]) {
  128. ++this._depth;
  129. } else if (depthDecrement[chunk.name]) {
  130. --this._depth;
  131. }
  132. }
  133. this._prev = chunk.name;
  134. callback(null);
  135. }
  136. }
  137. Stringer.stringer = Stringer.make;
  138. Stringer.make.Constructor = Stringer;
  139. module.exports = Stringer;