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;