jsesc.js 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329
  1. 'use strict';
  2. const object = {};
  3. const hasOwnProperty = object.hasOwnProperty;
  4. const forOwn = (object, callback) => {
  5. for (const key in object) {
  6. if (hasOwnProperty.call(object, key)) {
  7. callback(key, object[key]);
  8. }
  9. }
  10. };
  11. const extend = (destination, source) => {
  12. if (!source) {
  13. return destination;
  14. }
  15. forOwn(source, (key, value) => {
  16. destination[key] = value;
  17. });
  18. return destination;
  19. };
  20. const forEach = (array, callback) => {
  21. const length = array.length;
  22. let index = -1;
  23. while (++index < length) {
  24. callback(array[index]);
  25. }
  26. };
  27. const toString = object.toString;
  28. const isArray = Array.isArray;
  29. const isBuffer = Buffer.isBuffer;
  30. const isObject = (value) => {
  31. // This is a very simple check, but it’s good enough for what we need.
  32. return toString.call(value) == '[object Object]';
  33. };
  34. const isString = (value) => {
  35. return typeof value == 'string' ||
  36. toString.call(value) == '[object String]';
  37. };
  38. const isNumber = (value) => {
  39. return typeof value == 'number' ||
  40. toString.call(value) == '[object Number]';
  41. };
  42. const isFunction = (value) => {
  43. return typeof value == 'function';
  44. };
  45. const isMap = (value) => {
  46. return toString.call(value) == '[object Map]';
  47. };
  48. const isSet = (value) => {
  49. return toString.call(value) == '[object Set]';
  50. };
  51. /*--------------------------------------------------------------------------*/
  52. // https://mathiasbynens.be/notes/javascript-escapes#single
  53. const singleEscapes = {
  54. '"': '\\"',
  55. '\'': '\\\'',
  56. '\\': '\\\\',
  57. '\b': '\\b',
  58. '\f': '\\f',
  59. '\n': '\\n',
  60. '\r': '\\r',
  61. '\t': '\\t'
  62. // `\v` is omitted intentionally, because in IE < 9, '\v' == 'v'.
  63. // '\v': '\\x0B'
  64. };
  65. const regexSingleEscape = /["'\\\b\f\n\r\t]/;
  66. const regexDigit = /[0-9]/;
  67. const regexWhitelist = /[ !#-&\(-\[\]-_a-~]/;
  68. const jsesc = (argument, options) => {
  69. const increaseIndentation = () => {
  70. oldIndent = indent;
  71. ++options.indentLevel;
  72. indent = options.indent.repeat(options.indentLevel)
  73. };
  74. // Handle options
  75. const defaults = {
  76. 'escapeEverything': false,
  77. 'minimal': false,
  78. 'isScriptContext': false,
  79. 'quotes': 'single',
  80. 'wrap': false,
  81. 'es6': false,
  82. 'json': false,
  83. 'compact': true,
  84. 'lowercaseHex': false,
  85. 'numbers': 'decimal',
  86. 'indent': '\t',
  87. 'indentLevel': 0,
  88. '__inline1__': false,
  89. '__inline2__': false
  90. };
  91. const json = options && options.json;
  92. if (json) {
  93. defaults.quotes = 'double';
  94. defaults.wrap = true;
  95. }
  96. options = extend(defaults, options);
  97. if (
  98. options.quotes != 'single' &&
  99. options.quotes != 'double' &&
  100. options.quotes != 'backtick'
  101. ) {
  102. options.quotes = 'single';
  103. }
  104. const quote = options.quotes == 'double' ?
  105. '"' :
  106. (options.quotes == 'backtick' ?
  107. '`' :
  108. '\''
  109. );
  110. const compact = options.compact;
  111. const lowercaseHex = options.lowercaseHex;
  112. let indent = options.indent.repeat(options.indentLevel);
  113. let oldIndent = '';
  114. const inline1 = options.__inline1__;
  115. const inline2 = options.__inline2__;
  116. const newLine = compact ? '' : '\n';
  117. let result;
  118. let isEmpty = true;
  119. const useBinNumbers = options.numbers == 'binary';
  120. const useOctNumbers = options.numbers == 'octal';
  121. const useDecNumbers = options.numbers == 'decimal';
  122. const useHexNumbers = options.numbers == 'hexadecimal';
  123. if (json && argument && isFunction(argument.toJSON)) {
  124. argument = argument.toJSON();
  125. }
  126. if (!isString(argument)) {
  127. if (isMap(argument)) {
  128. if (argument.size == 0) {
  129. return 'new Map()';
  130. }
  131. if (!compact) {
  132. options.__inline1__ = true;
  133. options.__inline2__ = false;
  134. }
  135. return 'new Map(' + jsesc(Array.from(argument), options) + ')';
  136. }
  137. if (isSet(argument)) {
  138. if (argument.size == 0) {
  139. return 'new Set()';
  140. }
  141. return 'new Set(' + jsesc(Array.from(argument), options) + ')';
  142. }
  143. if (isBuffer(argument)) {
  144. if (argument.length == 0) {
  145. return 'Buffer.from([])';
  146. }
  147. return 'Buffer.from(' + jsesc(Array.from(argument), options) + ')';
  148. }
  149. if (isArray(argument)) {
  150. result = [];
  151. options.wrap = true;
  152. if (inline1) {
  153. options.__inline1__ = false;
  154. options.__inline2__ = true;
  155. }
  156. if (!inline2) {
  157. increaseIndentation();
  158. }
  159. forEach(argument, (value) => {
  160. isEmpty = false;
  161. if (inline2) {
  162. options.__inline2__ = false;
  163. }
  164. result.push(
  165. (compact || inline2 ? '' : indent) +
  166. jsesc(value, options)
  167. );
  168. });
  169. if (isEmpty) {
  170. return '[]';
  171. }
  172. if (inline2) {
  173. return '[' + result.join(', ') + ']';
  174. }
  175. return '[' + newLine + result.join(',' + newLine) + newLine +
  176. (compact ? '' : oldIndent) + ']';
  177. } else if (isNumber(argument)) {
  178. if (json) {
  179. // Some number values (e.g. `Infinity`) cannot be represented in JSON.
  180. return JSON.stringify(argument);
  181. }
  182. if (useDecNumbers) {
  183. return String(argument);
  184. }
  185. if (useHexNumbers) {
  186. let hexadecimal = argument.toString(16);
  187. if (!lowercaseHex) {
  188. hexadecimal = hexadecimal.toUpperCase();
  189. }
  190. return '0x' + hexadecimal;
  191. }
  192. if (useBinNumbers) {
  193. return '0b' + argument.toString(2);
  194. }
  195. if (useOctNumbers) {
  196. return '0o' + argument.toString(8);
  197. }
  198. } else if (!isObject(argument)) {
  199. if (json) {
  200. // For some values (e.g. `undefined`, `function` objects),
  201. // `JSON.stringify(value)` returns `undefined` (which isn’t valid
  202. // JSON) instead of `'null'`.
  203. return JSON.stringify(argument) || 'null';
  204. }
  205. return String(argument);
  206. } else { // it’s an object
  207. result = [];
  208. options.wrap = true;
  209. increaseIndentation();
  210. forOwn(argument, (key, value) => {
  211. isEmpty = false;
  212. result.push(
  213. (compact ? '' : indent) +
  214. jsesc(key, options) + ':' +
  215. (compact ? '' : ' ') +
  216. jsesc(value, options)
  217. );
  218. });
  219. if (isEmpty) {
  220. return '{}';
  221. }
  222. return '{' + newLine + result.join(',' + newLine) + newLine +
  223. (compact ? '' : oldIndent) + '}';
  224. }
  225. }
  226. const string = argument;
  227. // Loop over each code unit in the string and escape it
  228. let index = -1;
  229. const length = string.length;
  230. result = '';
  231. while (++index < length) {
  232. const character = string.charAt(index);
  233. if (options.es6) {
  234. const first = string.charCodeAt(index);
  235. if ( // check if it’s the start of a surrogate pair
  236. first >= 0xD800 && first <= 0xDBFF && // high surrogate
  237. length > index + 1 // there is a next code unit
  238. ) {
  239. const second = string.charCodeAt(index + 1);
  240. if (second >= 0xDC00 && second <= 0xDFFF) { // low surrogate
  241. // https://mathiasbynens.be/notes/javascript-encoding#surrogate-formulae
  242. const codePoint = (first - 0xD800) * 0x400 + second - 0xDC00 + 0x10000;
  243. let hexadecimal = codePoint.toString(16);
  244. if (!lowercaseHex) {
  245. hexadecimal = hexadecimal.toUpperCase();
  246. }
  247. result += '\\u{' + hexadecimal + '}';
  248. ++index;
  249. continue;
  250. }
  251. }
  252. }
  253. if (!options.escapeEverything) {
  254. if (regexWhitelist.test(character)) {
  255. // It’s a printable ASCII character that is not `"`, `'` or `\`,
  256. // so don’t escape it.
  257. result += character;
  258. continue;
  259. }
  260. if (character == '"') {
  261. result += quote == character ? '\\"' : character;
  262. continue;
  263. }
  264. if (character == '`') {
  265. result += quote == character ? '\\`' : character;
  266. continue;
  267. }
  268. if (character == '\'') {
  269. result += quote == character ? '\\\'' : character;
  270. continue;
  271. }
  272. }
  273. if (
  274. character == '\0' &&
  275. !json &&
  276. !regexDigit.test(string.charAt(index + 1))
  277. ) {
  278. result += '\\0';
  279. continue;
  280. }
  281. if (regexSingleEscape.test(character)) {
  282. // no need for a `hasOwnProperty` check here
  283. result += singleEscapes[character];
  284. continue;
  285. }
  286. const charCode = character.charCodeAt(0);
  287. if (options.minimal && charCode != 0x2028 && charCode != 0x2029) {
  288. result += character;
  289. continue;
  290. }
  291. let hexadecimal = charCode.toString(16);
  292. if (!lowercaseHex) {
  293. hexadecimal = hexadecimal.toUpperCase();
  294. }
  295. const longhand = hexadecimal.length > 2 || json;
  296. const escaped = '\\' + (longhand ? 'u' : 'x') +
  297. ('0000' + hexadecimal).slice(longhand ? -4 : -2);
  298. result += escaped;
  299. continue;
  300. }
  301. if (options.wrap) {
  302. result = quote + result + quote;
  303. }
  304. if (quote == '`') {
  305. result = result.replace(/\$\{/g, '\\\$\{');
  306. }
  307. if (options.isScriptContext) {
  308. // https://mathiasbynens.be/notes/etago
  309. return result
  310. .replace(/<\/(script|style)/gi, '<\\/$1')
  311. .replace(/<!--/g, json ? '\\u003C!--' : '\\x3C!--');
  312. }
  313. return result;
  314. };
  315. jsesc.version = '2.5.2';
  316. module.exports = jsesc;