11const util = require ( 'util' )
22
3- function quote ( value ) {
4- if ( typeof value !== 'string' ) value = JSON . stringify ( value )
5- return JSON . stringify ( value )
3+ exports . escape = function escape ( value , options ) {
4+ // The JSON-stringify string serialization algorithm, but generalized for string delimiters
5+ // (e.g. " or ') and different escape characters (e.g. Powershell uses `)
6+ // https://tc39.es/ecma262/multipage/structured-data.html#sec-quotejsonstring
7+ const {
8+ delimiter = '"' ,
9+ escapeChar = '\\' ,
10+ escapeNewlines = true
11+ } = options || { }
12+
13+ return [ ...value ] . map ( ( c ) => {
14+ if ( c === '\b' ) {
15+ return escapeChar + 'b'
16+ } else if ( c === '\t' ) {
17+ return escapeChar + 't'
18+ } else if ( c === '\n' ) {
19+ if ( escapeNewlines ) {
20+ return escapeChar + 'n'
21+ } else {
22+ return c // Don't just continue, or this is caught by < \u0020
23+ }
24+ } else if ( c === '\f' ) {
25+ return escapeChar + 'f'
26+ } else if ( c === '\r' ) {
27+ if ( escapeNewlines ) {
28+ return escapeChar + 'r'
29+ } else {
30+ return c // Don't just continue, or this is caught by < \u0020
31+ }
32+ } else if ( c === escapeChar ) {
33+ return escapeChar + escapeChar
34+ } else if ( c === delimiter ) {
35+ return escapeChar + delimiter
36+ } else if ( c < '\u0020' || c > '\u007E' ) {
37+ // Delegate the trickier non-ASCII cases to the normal algorithm. Some of these are escaped as
38+ // \uXXXX, whilst others are represented literally. Since we're using this primarily for header
39+ // values that are generally (though not strictly?) ASCII-only, this should almost never happen.
40+ return JSON . stringify ( c ) . slice ( 1 , - 1 )
41+ } else {
42+ return c
43+ }
44+ } ) . join ( '' )
645}
746
8- function escape ( value ) {
9- const q = quote ( value )
10- return q && q . slice ( 1 , - 1 )
47+ function doubleQuoteEscape ( value ) {
48+ return exports . escape ( value , { delimiter : '"' } )
49+ }
50+
51+ function singleQuoteEscape ( value ) {
52+ return exports . escape ( value , { delimiter : "'" } )
1153}
1254
1355/**
14- * Wraps the `util.format` function and adds the %q and %v format options,
15- * where `%q` - escape tricky characters, like newline or quotes
16- * and `%v` - JSON-stringify-if-necessary
56+ * Wraps the `util.format` function and adds the %qd, %qs and %v format options,
57+ * where `%qd` escapes characters for a single-line double-quoted string, `%qs`
58+ * escapes characters for a single-line single-quoted string, and `%v`
59+ * JSON-stringifies the value.
1760 *
1861 * @param {string } value
1962 * @param {...string } format
2063 *
21- * @example
22- * format('foo("%q")', { bar: 'baz' })
23- * // output: foo("{\"bar\":\"baz\"}")
24- *
25- * format('foo(%v)', { bar: 'baz' })
26- * // output: foo({"bar":"baz"})
27- *
2864 * @returns {string } Formatted string
2965 */
30- function format ( value , ...format ) {
66+ exports . format = function format ( value , ...format ) {
3167 if ( typeof value !== 'string' ) return ''
3268
3369 let i = 0
34- value = value . replace ( / (?< ! % ) % [ s d i f j o O c q v ] / g, ( m ) => {
70+ value = value . replace ( / (?< ! % ) % ( [ s d i f j o O c v ] | q [ s d ] ) / g, ( m ) => {
3571 // JSON-stringify
3672 if ( m === '%v' ) {
3773 const [ elem ] = format . splice ( i , 1 )
3874 return JSON . stringify ( elem )
3975 }
40- // JSON-stringify, remove quotes (means, escape)
41- if ( m === '%q' ) {
76+ // Escape for double-quoted string
77+ if ( m === '%qd' ) {
78+ const [ elem ] = format . splice ( i , 1 )
79+ return doubleQuoteEscape ( elem )
80+ }
81+ // Escape for single-quoted string
82+ if ( m === '%qs' ) {
4283 const [ elem ] = format . splice ( i , 1 )
43- return escape ( elem )
84+ return singleQuoteEscape ( elem )
4485 }
4586 i += 1
4687 return m
@@ -49,5 +90,3 @@ function format (value, ...format) {
4990 const ret = util . format ( value , ...format )
5091 return ret
5192}
52-
53- module . exports = format
0 commit comments