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