diff --git a/index.js b/index.js index bfc3160..84f7a67 100644 --- a/index.js +++ b/index.js @@ -1,14 +1,47 @@ const { isNull, isBoolean, isNumber, isString, isArray, isObject, isEmpty, fromPairs, keys, map, repeat } = require('lodash') const { parse: parseLua } = require('luaparse') -const formatLuaString = (string, singleQuote) => (singleQuote ? `'${string.replace(/'/g, "\\'")}'` : `"${string.replace(/"/g, '\\"')}"`) +/** + * @link https://github.com/fstirlitz/luaparse/blob/0f525b152516bc8afa34564de2423b039aa83bc1/luaparse.js#L1414 + * @param {string} id + * @returns {boolean} + */ +function isKeyword(id) { + switch (id.length) { + case 2: + return 'do' === id || 'if' === id || 'in' === id || 'or' === id; + case 3: + return 'and' === id || 'end' === id || 'for' === id || 'not' === id; + case 4: + // TODO: configure labels "goto" + return 'else' === id || 'then' === id || 'goto' === id; + case 5: + return 'break' === id || 'local' === id || 'until' === id || 'while' === id; + case 6: + return 'elseif' === id || 'repeat' === id || 'return' === id; + case 8: + return 'function' === id; + } + return false; +} + +const formatLuaString = (string, singleQuote, multilineString) => { + string = string.replace(/\\/g, "\\\\") + if (multilineString) { + if(string.includes("]]")) throw new SyntaxError("Multiline string can't include ']]'.") // TODO: provide more information about the error + return `[[${string}]]` + } else { + string = string.replace(/\n/g, "\\n") + return (singleQuote ? `'${string.replace(/'/g, "\\'")}'` : `"${string.replace(/"/g, '\\"')}"`) + } +} const valueKeys = { false: 'false', true: 'true', null: 'nil' } const formatLuaKey = (string, singleQuote) => - valueKeys[string] ? `[${valueKeys[string]}]` : string.match(/^[a-zA-Z_][a-zA-Z_0-9]*$/) ? string : `[${formatLuaString(string, singleQuote)}]` + valueKeys[string] ? `[${valueKeys[string]}]` : (string.match(/^[a-zA-Z_][a-zA-Z_0-9]*$/) && !isKeyword(string)) ? string : `[${formatLuaString(string, singleQuote)}]` -const format = (value, options = { eol: '\n', singleQuote: true, spaces: 2 }) => { +const format = (value, options = { eol: '\n', singleQuote: true, multilineString: false, spaces: 2 }) => { options = options || {} const eol = (options.eol = isString(options.eol) ? options.eol : '\n') options.singleQuote = isBoolean(options.singleQuote) ? options.singleQuote : true @@ -22,7 +55,7 @@ const format = (value, options = { eol: '\n', singleQuote: true, spaces: 2 }) => return value.toString() } if (isString(value)) { - return formatLuaString(value, options.singleQuote) + return formatLuaString(value, options.singleQuote, options.multilineString) } if (isArray(value)) { if (isEmpty(value)) { @@ -43,11 +76,11 @@ const format = (value, options = { eol: '\n', singleQuote: true, spaces: 2 }) => const spaces = isNumber(options.spaces) ? repeat(' ', options.spaces * (i + 1)) : repeat(options.spaces, i + 1) const spacesEnd = isNumber(options.spaces) ? repeat(' ', options.spaces * i) : repeat(options.spaces, i) return `{${eol}${keys(value) - .map(key => `${spaces}${formatLuaKey(key, options.singleQuote)} = ${rec(value[key], i + 1)},`) + .map(key => `${spaces}${formatLuaKey(key, options.singleQuote, false)} = ${rec(value[key], i + 1)},`) .join(eol)}${eol}${spacesEnd}}` } return `{${keys(value) - .map(key => `${formatLuaKey(key, options.singleQuote)}=${rec(value[key], i + 1)},`) + .map(key => `${formatLuaKey(key, options.singleQuote, false)}=${rec(value[key], i + 1)},`) .join('')}}` } throw new Error(`can't format ${typeof value}`) diff --git a/test.js b/test.js index ceffaf3..cbdea86 100644 --- a/test.js +++ b/test.js @@ -13,6 +13,12 @@ const json = { _float: 3.14, _big: 1e123, _string: "...'...'...", + _multilinestring: '1st line\n2nd line\n3rd line', + _backslash: { + _single: '\\', + _double: '\\\\', + _followed: '\\test', + }, _array0: [], _array1: [1], _array2: [1, 2], @@ -29,6 +35,27 @@ const json = { false: 4, true: 5, }, + _keywords: { + do: 'keyword', + if: 'keyword', + in: 'keyword', + or: 'keyword', + and: 'keyword', + end: 'keyword', + for: 'keyword', + not: 'keyword', + else: 'keyword', + then: 'keyword', + goto: 'keyword', + break: 'keyword', + local: 'keyword', + until: 'keyword', + while: 'keyword', + elseif: 'keyword', + repeat: 'keyword', + return: 'keyword', + function: 'keyword' + } } const lua = `return { @@ -41,6 +68,12 @@ const lua = `return { _float = 3.14, _big = 1e+123, _string = '...\\'...\\'...', + _multilinestring = '1st line\\n2nd line\\n3rd line', + _backslash = { + _single = '\\\\', + _double = '\\\\\\\\', + _followed = '\\\\test', + }, _array0 = {}, _array1 = { 1, @@ -62,9 +95,30 @@ const lua = `return { [false] = 4, [true] = 5, }, + _keywords = { + ['do'] = 'keyword', + ['if'] = 'keyword', + ['in'] = 'keyword', + ['or'] = 'keyword', + ['and'] = 'keyword', + ['end'] = 'keyword', + ['for'] = 'keyword', + ['not'] = 'keyword', + ['else'] = 'keyword', + ['then'] = 'keyword', + ['goto'] = 'keyword', + ['break'] = 'keyword', + ['local'] = 'keyword', + ['until'] = 'keyword', + ['while'] = 'keyword', + ['elseif'] = 'keyword', + ['repeat'] = 'keyword', + ['return'] = 'keyword', + ['function'] = 'keyword', + }, }` -const luaMinified = `return{_null=nil,_false=false,_true=true,_zero=0,_one=1,_negative=-1,_float=3.14,_big=1e+123,_string='...\\'...\\'...',_array0={},_array1={1,},_array2={1,2,},_object0={},_object1={_nested={_value=1,},},_object2={['a b']=1,['...\\'...\\'...']=2,[nil]=3,[false]=4,[true]=5,},}` +const luaMinified = `return{_null=nil,_false=false,_true=true,_zero=0,_one=1,_negative=-1,_float=3.14,_big=1e+123,_string='...\\'...\\'...',_multilinestring='1st line\\n2nd line\\n3rd line',_backslash={_single='\\\\',_double='\\\\\\\\',_followed='\\\\test',},_array0={},_array1={1,},_array2={1,2,},_object0={},_object1={_nested={_value=1,},},_object2={['a b']=1,['...\\'...\\'...']=2,[nil]=3,[false]=4,[true]=5,},_keywords={['do']='keyword',['if']='keyword',['in']='keyword',['or']='keyword',['and']='keyword',['end']='keyword',['for']='keyword',['not']='keyword',['else']='keyword',['then']='keyword',['goto']='keyword',['break']='keyword',['local']='keyword',['until']='keyword',['while']='keyword',['elseif']='keyword',['repeat']='keyword',['return']='keyword',['function']='keyword',},}` equal(format(json), lua) deepEqual(parse(lua), json) @@ -77,3 +131,12 @@ jsonDoubleQuote._object2 = mapKeys(jsonDoubleQuote._object2, (_, key) => key.rep const luaDoubleQuote = lua.replace(/'/g, '"') equal(format(jsonDoubleQuote, { singleQuote: false }), luaDoubleQuote) deepEqual(parse(luaDoubleQuote), jsonDoubleQuote) + +const jsonMultiline = { "test": "1st line\n2nd line\n3rd line" } +const luaMultiline = `return { + test = [[1st line +2nd line +3rd line]], +}` +equal(format(jsonMultiline, { multilineString: true }), luaMultiline) +deepEqual(parse(luaMultiline), jsonMultiline)