Skip to content

Commit

Permalink
feat: export unescape functions
Browse files Browse the repository at this point in the history
feat: unescape unicode chars
  • Loading branch information
mdvorak committed Mar 18, 2023
1 parent 1e6485e commit a1d9a97
Show file tree
Hide file tree
Showing 2 changed files with 125 additions and 27 deletions.
53 changes: 50 additions & 3 deletions src/properties.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import * as properties from '.'
import properties from '.'

describe('parse', () => {
it('should parse all lines', () => {
Expand Down Expand Up @@ -129,10 +129,10 @@ describe('data access', () => {
['foo9=', 'bar9', 'foo9\\==bar9'],
['foo10=', 'bar10', 'foo10\\==bar10'],
['foo11 ', 'bar11', 'foo11\\ =bar11'],
[' foo12', 'bar12 ', '\\ foo12=bar12\\ '],
[' foo12', 'bar12 ', '\\ foo12=bar12 '],
['#foo13', 'bar13', '\\#foo13=bar13'],
['!foo14#', 'bar14', '\\!foo14\\#=bar14'],
['foo15', '#bar15', 'foo15=#bar15'],
['foo15', '#bar15', 'foo15=\\#bar15'],
['f o o18', ' bar18', 'f\\ o\\ \\ o18=\\ bar18'],
['foo19\n', 'bar\t\f\r19\n', 'foo19\\n=bar\\t\\f\\r19\\n'],
['foo20', '', 'foo20='],
Expand Down Expand Up @@ -252,3 +252,50 @@ describe('data access', () => {
])
})
})


describe('The property key escaping', () => {
it.each([
['foo1', 'foo1'],
['foo2:', 'foo2\\:'],
['foo3=', 'foo3\\='],
['foo4\t', 'foo4\\t'],
['foo5 ', 'foo5\\ '],
[' foo6', '\\ foo6'],
['#foo7', '\\#foo7'],
['!foo8#', '\\!foo8\\#'],
['fo o9', 'fo\\ \\ o9'],
['foo10\n', 'foo10\\n'],
['f\r\f\n\too11', 'f\\r\\f\\n\\too11'],
['\\foo12\\', '\\\\foo12\\\\'],
['\0\u0001', '\\u0000\\u0001'],
['\u3053\u3093\u306B\u3061\u306F', '\\u3053\\u3093\\u306b\\u3061\\u306f'],
['こんにちは', '\\u3053\\u3093\\u306b\\u3061\\u306f'],
])('should escape key "%s" as "%s"', (key: string, expected: string) => {
const result = properties.escapeKey(key)
expect(result).toEqual(expected)
})
})

describe('The property value escaping', () => {
it.each([
['foo1', 'foo1'],
['foo2:', 'foo2\\:'],
['foo3=', 'foo3\\='],
['foo4\t', 'foo4\\t'],
['foo5 ', 'foo5 '],
[' foo6', '\\ foo6'],
['#foo7', '\\#foo7'],
['!foo8#', '\\!foo8\\#'],
['fo o9', 'fo o9'],
['foo10\n', 'foo10\\n'],
['f\r\f\n\too11', 'f\\r\\f\\n\\too11'],
['\\foo12\\', '\\\\foo12\\\\'],
['\0\u0001', '\\u0000\\u0001'],
['\u3053\u3093\u306B\u3061\u306F', '\\u3053\\u3093\\u306b\\u3061\\u306f'],
['こんにちは', '\\u3053\\u3093\\u306b\\u3061\\u306f'],
])('should escape value "%s" as "%s"', (key: string, expected: string) => {
const result = properties.escapeValue(key)
expect(result).toEqual(expected)
})
})
99 changes: 75 additions & 24 deletions src/properties.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ export const stringify = (config: Properties): string => {
*/
export function* list(config: Properties): Generator<KeyValuePair> {
for (const {key, rawValue} of listPairs(config.lines)) {
yield {key, value: unescapeValue(rawValue)}
yield {key, value: unescape(rawValue)}
}
}

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

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

for (const {key, rawValue} of listPairs(config.lines)) {
result.set(key, unescapeValue(rawValue))
result.set(key, unescape(rawValue))
}

return result
Expand Down Expand Up @@ -308,52 +308,105 @@ const unescapeChar = (c: string): string => {
}
}

const unescapeValue = (str: string): string =>
/**
* Unescape key or value.
*
* @param str Escaped string.
* @return Actual string.
*/
export const unescape = (str: string): string =>
str.replace(/\\(.)/g, s => unescapeChar(s[1]))

// Very simple implementation, does not handle unicode etc
const escapeValue = (str: string, escapeChars = ''): string => {
/**
* Escape property key.
*
* @param unescapedKey Property key to be escaped.
* @param escapeUnicode Escape unicode chars (below 0x0020 and above 0x007e). Default is true.
* @return Escaped string.
*/
export const escapeKey = (unescapedKey: string, escapeUnicode = true): string => {
return escape(unescapedKey, true, escapeUnicode)
}

/**
* Escape property value.
*
* @param unescapedValue Property value to be escaped.
* @param escapeUnicode Escape unicode chars (below 0x0020 and above 0x007e). Default is true.
* @return Escaped string.
*/
export const escapeValue = (unescapedValue: string, escapeUnicode = true): string => {
return escape(unescapedValue, false, escapeUnicode)
}

/**
* Internal escape method.
*
* @param unescapedContent Text to be escaped.
* @param escapeSpace Whether all spaces should be escaped
* @param escapeUnicode Whether unicode chars should be escaped
* @return Escaped string.
*/
const escape = (
unescapedContent: string,
escapeSpace: boolean,
escapeUnicode: boolean
): string => {
const result: string[] = []
let escapeNext = str.startsWith(' ') // always escape space at beginning

for (let index = 0; index < str.length; index++) {
const char = str[index]
// eslint-disable-next-line unicorn/no-for-loop
for (let index = 0; index < unescapedContent.length; index++) {
const char = unescapedContent[index]
switch (char) {
case ' ': {
// Escape space if required, or if it is first character
if (escapeSpace || index === 0) {
result.push('\\ ')
} else {
result.push(' ')
}
break
}
case '\\': {
result.push('\\\\')
break
}
case '\f': {
// Formfeed/
// Form-feed
result.push('\\f')
break
}
case '\n': {
// Newline.
// Newline
result.push('\\n')
break
}
case '\r': {
// Carriage return.
// Carriage return
result.push('\\r')
break
}
case '\t': {
// Tab.
// Tab
result.push('\\t')
break
}
case '=': // Fall through
case ':': // Fall through
case '#': // Fall through
case '!': {
result.push('\\', char)
break
}
default: {
// Escape trailing space
if (index === str.length - 1 && char === ' ') {
escapeNext = true
if (escapeUnicode) {
const codePoint: number = char.codePointAt(0) as number // can never be undefined
if (codePoint < 0x0020 || codePoint > 0x007e) {
result.push('\\u', codePoint.toString(16).padStart(4, '0'))
break
}
}
// Escape if required
if (escapeNext || escapeChars.includes(char)) {
result.push('\\')
escapeNext = false
}

// Normal char
result.push(char)
break
}
Expand All @@ -362,5 +415,3 @@ const escapeValue = (str: string, escapeChars = ''): string => {

return result.join('')
}

const escapeKey = (str: string): string => escapeValue(str, ' #!:=')

0 comments on commit a1d9a97

Please sign in to comment.