Skip to content

Commit a1d9a97

Browse files
committed
feat: export unescape functions
feat: unescape unicode chars
1 parent 1e6485e commit a1d9a97

File tree

2 files changed

+125
-27
lines changed

2 files changed

+125
-27
lines changed

src/properties.spec.ts

+50-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import * as properties from '.'
1+
import properties from '.'
22

33
describe('parse', () => {
44
it('should parse all lines', () => {
@@ -129,10 +129,10 @@ describe('data access', () => {
129129
['foo9=', 'bar9', 'foo9\\==bar9'],
130130
['foo10=', 'bar10', 'foo10\\==bar10'],
131131
['foo11 ', 'bar11', 'foo11\\ =bar11'],
132-
[' foo12', 'bar12 ', '\\ foo12=bar12\\ '],
132+
[' foo12', 'bar12 ', '\\ foo12=bar12 '],
133133
['#foo13', 'bar13', '\\#foo13=bar13'],
134134
['!foo14#', 'bar14', '\\!foo14\\#=bar14'],
135-
['foo15', '#bar15', 'foo15=#bar15'],
135+
['foo15', '#bar15', 'foo15=\\#bar15'],
136136
['f o o18', ' bar18', 'f\\ o\\ \\ o18=\\ bar18'],
137137
['foo19\n', 'bar\t\f\r19\n', 'foo19\\n=bar\\t\\f\\r19\\n'],
138138
['foo20', '', 'foo20='],
@@ -252,3 +252,50 @@ describe('data access', () => {
252252
])
253253
})
254254
})
255+
256+
257+
describe('The property key escaping', () => {
258+
it.each([
259+
['foo1', 'foo1'],
260+
['foo2:', 'foo2\\:'],
261+
['foo3=', 'foo3\\='],
262+
['foo4\t', 'foo4\\t'],
263+
['foo5 ', 'foo5\\ '],
264+
[' foo6', '\\ foo6'],
265+
['#foo7', '\\#foo7'],
266+
['!foo8#', '\\!foo8\\#'],
267+
['fo o9', 'fo\\ \\ o9'],
268+
['foo10\n', 'foo10\\n'],
269+
['f\r\f\n\too11', 'f\\r\\f\\n\\too11'],
270+
['\\foo12\\', '\\\\foo12\\\\'],
271+
['\0\u0001', '\\u0000\\u0001'],
272+
['\u3053\u3093\u306B\u3061\u306F', '\\u3053\\u3093\\u306b\\u3061\\u306f'],
273+
['こんにちは', '\\u3053\\u3093\\u306b\\u3061\\u306f'],
274+
])('should escape key "%s" as "%s"', (key: string, expected: string) => {
275+
const result = properties.escapeKey(key)
276+
expect(result).toEqual(expected)
277+
})
278+
})
279+
280+
describe('The property value escaping', () => {
281+
it.each([
282+
['foo1', 'foo1'],
283+
['foo2:', 'foo2\\:'],
284+
['foo3=', 'foo3\\='],
285+
['foo4\t', 'foo4\\t'],
286+
['foo5 ', 'foo5 '],
287+
[' foo6', '\\ foo6'],
288+
['#foo7', '\\#foo7'],
289+
['!foo8#', '\\!foo8\\#'],
290+
['fo o9', 'fo o9'],
291+
['foo10\n', 'foo10\\n'],
292+
['f\r\f\n\too11', 'f\\r\\f\\n\\too11'],
293+
['\\foo12\\', '\\\\foo12\\\\'],
294+
['\0\u0001', '\\u0000\\u0001'],
295+
['\u3053\u3093\u306B\u3061\u306F', '\\u3053\\u3093\\u306b\\u3061\\u306f'],
296+
['こんにちは', '\\u3053\\u3093\\u306b\\u3061\\u306f'],
297+
])('should escape value "%s" as "%s"', (key: string, expected: string) => {
298+
const result = properties.escapeValue(key)
299+
expect(result).toEqual(expected)
300+
})
301+
})

src/properties.ts

+75-24
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ export const stringify = (config: Properties): string => {
7777
*/
7878
export function* list(config: Properties): Generator<KeyValuePair> {
7979
for (const {key, rawValue} of listPairs(config.lines)) {
80-
yield {key, value: unescapeValue(rawValue)}
80+
yield {key, value: unescape(rawValue)}
8181
}
8282
}
8383

@@ -91,7 +91,7 @@ export function* list(config: Properties): Generator<KeyValuePair> {
9191
export const get = (config: Properties, key: string): string | undefined => {
9292
// Find existing
9393
const {rawValue} = findValue(config.lines, key)
94-
return typeof rawValue === 'string' ? unescapeValue(rawValue) : undefined
94+
return typeof rawValue === 'string' ? unescape(rawValue) : undefined
9595
}
9696

9797
/**
@@ -105,7 +105,7 @@ export const toMap = (config: Properties): Map<string, string> => {
105105
const result = new Map<string, string>()
106106

107107
for (const {key, rawValue} of listPairs(config.lines)) {
108-
result.set(key, unescapeValue(rawValue))
108+
result.set(key, unescape(rawValue))
109109
}
110110

111111
return result
@@ -308,52 +308,105 @@ const unescapeChar = (c: string): string => {
308308
}
309309
}
310310

311-
const unescapeValue = (str: string): string =>
311+
/**
312+
* Unescape key or value.
313+
*
314+
* @param str Escaped string.
315+
* @return Actual string.
316+
*/
317+
export const unescape = (str: string): string =>
312318
str.replace(/\\(.)/g, s => unescapeChar(s[1]))
313319

314-
// Very simple implementation, does not handle unicode etc
315-
const escapeValue = (str: string, escapeChars = ''): string => {
320+
/**
321+
* Escape property key.
322+
*
323+
* @param unescapedKey Property key to be escaped.
324+
* @param escapeUnicode Escape unicode chars (below 0x0020 and above 0x007e). Default is true.
325+
* @return Escaped string.
326+
*/
327+
export const escapeKey = (unescapedKey: string, escapeUnicode = true): string => {
328+
return escape(unescapedKey, true, escapeUnicode)
329+
}
330+
331+
/**
332+
* Escape property value.
333+
*
334+
* @param unescapedValue Property value to be escaped.
335+
* @param escapeUnicode Escape unicode chars (below 0x0020 and above 0x007e). Default is true.
336+
* @return Escaped string.
337+
*/
338+
export const escapeValue = (unescapedValue: string, escapeUnicode = true): string => {
339+
return escape(unescapedValue, false, escapeUnicode)
340+
}
341+
342+
/**
343+
* Internal escape method.
344+
*
345+
* @param unescapedContent Text to be escaped.
346+
* @param escapeSpace Whether all spaces should be escaped
347+
* @param escapeUnicode Whether unicode chars should be escaped
348+
* @return Escaped string.
349+
*/
350+
const escape = (
351+
unescapedContent: string,
352+
escapeSpace: boolean,
353+
escapeUnicode: boolean
354+
): string => {
316355
const result: string[] = []
317-
let escapeNext = str.startsWith(' ') // always escape space at beginning
318356

319-
for (let index = 0; index < str.length; index++) {
320-
const char = str[index]
357+
// eslint-disable-next-line unicorn/no-for-loop
358+
for (let index = 0; index < unescapedContent.length; index++) {
359+
const char = unescapedContent[index]
321360
switch (char) {
361+
case ' ': {
362+
// Escape space if required, or if it is first character
363+
if (escapeSpace || index === 0) {
364+
result.push('\\ ')
365+
} else {
366+
result.push(' ')
367+
}
368+
break
369+
}
322370
case '\\': {
323371
result.push('\\\\')
324372
break
325373
}
326374
case '\f': {
327-
// Formfeed/
375+
// Form-feed
328376
result.push('\\f')
329377
break
330378
}
331379
case '\n': {
332-
// Newline.
380+
// Newline
333381
result.push('\\n')
334382
break
335383
}
336384
case '\r': {
337-
// Carriage return.
385+
// Carriage return
338386
result.push('\\r')
339387
break
340388
}
341389
case '\t': {
342-
// Tab.
390+
// Tab
343391
result.push('\\t')
344392
break
345393
}
394+
case '=': // Fall through
395+
case ':': // Fall through
396+
case '#': // Fall through
397+
case '!': {
398+
result.push('\\', char)
399+
break
400+
}
346401
default: {
347-
// Escape trailing space
348-
if (index === str.length - 1 && char === ' ') {
349-
escapeNext = true
402+
if (escapeUnicode) {
403+
const codePoint: number = char.codePointAt(0) as number // can never be undefined
404+
if (codePoint < 0x0020 || codePoint > 0x007e) {
405+
result.push('\\u', codePoint.toString(16).padStart(4, '0'))
406+
break
407+
}
350408
}
351-
// Escape if required
352-
if (escapeNext || escapeChars.includes(char)) {
353-
result.push('\\')
354-
escapeNext = false
355-
}
356-
409+
// Normal char
357410
result.push(char)
358411
break
359412
}
@@ -362,5 +415,3 @@ const escapeValue = (str: string, escapeChars = ''): string => {
362415

363416
return result.join('')
364417
}
365-
366-
const escapeKey = (str: string): string => escapeValue(str, ' #!:=')

0 commit comments

Comments
 (0)