From 4161f6756dadef6cd5a5c2e1e75804122e892949 Mon Sep 17 00:00:00 2001 From: Benoit Giannangeli Date: Thu, 16 Mar 2017 09:45:50 +0100 Subject: string.format --- README.md | 122 +---------------------- package.json | 3 +- src/lauxlib.js | 5 + src/llex.js | 8 +- src/lobject.js | 2 +- src/lstrlib.js | 290 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/ltablib.js | 2 +- src/luaconf.js | 21 +++- src/lutf8lib.js | 2 +- tests/lstrlib.js | 92 ++++++++++++++++++ 10 files changed, 418 insertions(+), 129 deletions(-) diff --git a/README.md b/README.md index 617fd40..84195cf 100644 --- a/README.md +++ b/README.md @@ -25,91 +25,10 @@ - [ ] userdata - [x] Tag Methods - [ ] C API - - [x] lua_absindex - - [x] lua_atpanic - - [x] lua_call - - [x] lua_callk - - [x] lua_checkstack - - [x] lua_compare - - [x] lua_concat - - [x] lua_copy - - [x] lua_createtable - - [x] lua_error - - [x] lua_getfield - - [x] lua_getglobal - - [x] lua_getmetatable - - [x] lua_gettable - - [x] lua_gettop - - [x] lua_insert - - [x] lua_isinteger - - [x] lua_isnil - - [x] lua_isnone - - [x] lua_isnoneornil - - [x] lua_isnumber - - [x] lua_isstring - - [x] lua_istable - - [x] lua_isyieldable - - [x] lua_len - - [x] lua_load - - [x] lua_newstate - - [x] lua_newtable - - [x] lua_newthread - - [x] lua_next - - [x] lua_numbertointeger - - [x] lua_pcall - - [x] lua_pop - - [x] lua_pushboolean - - [x] lua_pushglobaltable - - [x] lua_pushinteger - - [x] lua_pushjsclosure (lua_pushcclosure) - - [x] lua_pushjsfunction (lua_pushcfunction) - - [x] lua_pushliteral - - [x] lua_pushlstring - - [x] lua_pushnil - - [x] lua_pushnumber - - [x] lua_pushstring - - [x] lua_pushthread - - [x] lua_pushvalue - - [x] lua_rawequal - - [x] lua_rawget - - [x] lua_rawgeti - - [x] lua_rawlen - - [x] lua_rawset - - [x] lua_remove - - [x] lua_replace - - [x] lua_resume - - [x] lua_rotate - - [x] lua_setfield - - [x] lua_setglobal - - [x] lua_seti - - [x] lua_setmetatable - - [x] lua_settable - - [x] lua_settop - - [x] lua_setupvalue - - [x] lua_status - - [x] lua_stringtonumber - - [x] lua_toboolean - - [x] lua_tointeger - - [x] lua_tointegerx - - [x] lua_tolstring - - [x] lua_tonumber - - [x] lua_tonumberx - - [x] lua_topointer - - [x] lua_tostring - - [x] lua_tothread - - [x] lua_type - - [x] lua_typename - - [x] lua_upvalueindex - - [x] lua_version - - [x] lua_xmove - - [x] lua_yield - - [x] lua_yieldk + - [x] ... - [ ] lua_arith - [ ] lua_close - [ ] lua_dump - - [x] lua_gc (unavailable) - - [x] lua_getallocf (unavailable) - - [x] lua_getextraspace (unavailable) - [ ] lua_gethook - [ ] lua_gethookcount - [ ] lua_gethookmask @@ -143,42 +62,7 @@ - [ ] lua_upvalueid - [ ] lua_upvaluejoin - [ ] Auxiliary library - - [x] luaL_addlstring - - [x] luaL_addstring - - [x] luaL_addvalue - - [x] luaL_argcheck - - [x] luaL_argerror - - [x] luaL_buffinit - - [x] luaL_buffinitsize - - [x] luaL_callmeta - - [x] luaL_checkany - - [x] luaL_checkinteger - - [x] luaL_checklstring - - [x] luaL_checknumber - - [x] luaL_checkstack - - [x] luaL_checkstring - - [x] luaL_checktype - - [x] luaL_error - - [x] luaL_getmetafield - - [x] luaL_getsubtable - - [x] luaL_len - - [x] luaL_loadbuffer - - [x] luaL_loadbufferx - - [x] luaL_loadstring - - [x] luaL_newlib - - [x] luaL_newstate - - [x] luaL_openlibs - - [x] luaL_opt - - [x] luaL_optinteger - - [x] luaL_optlstring - - [x] luaL_prepbuffsize - - [x] luaL_pushresult - - [x] luaL_requiref - - [x] luaL_setfuncs - - [x] luaL_tolstring - - [x] luaL_typename - - [x] luaL_where - - [ ] luaL_addchar + - [x] ... - [ ] luaL_addsize - [ ] luaL_checkoption - [ ] luaL_checkudata @@ -215,6 +99,7 @@ - [ ] String - [x] string.byte - [x] string.char + - [x] string.format - [x] string.len - [x] string.lower - [x] string.rep @@ -222,7 +107,6 @@ - [x] string.upper - [ ] string.dump - [ ] string.find - - [ ] string.format - [ ] string.gmatch - [ ] string.gsub - [ ] string.match diff --git a/package.json b/package.json index d567a62..0167a15 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "tmp": "0.0.31" }, "dependencies": { - "seedrandom": "^2.4.2" + "seedrandom": "^2.4.2", + "sprintf-js": "^1.0.3" } } diff --git a/src/lauxlib.js b/src/lauxlib.js index e3afa60..f77fe5e 100644 --- a/src/lauxlib.js +++ b/src/lauxlib.js @@ -226,6 +226,10 @@ const luaL_pushresult = function(B) { lapi.lua_pushstring(L, B.b); }; +const luaL_addchar = function(B, c) { + B.b += c; +}; + const luaL_addvalue = function(B) { let L = B.L; let s = lapi.lua_tostring(L, -1); @@ -395,6 +399,7 @@ const luaL_newlib = function(L, l) { module.exports.LUA_LOADED_TABLE = LUA_LOADED_TABLE; module.exports.luaL_Buffer = luaL_Buffer; +module.exports.luaL_addchar = luaL_addchar; module.exports.luaL_addlstring = luaL_addlstring; module.exports.luaL_addstring = luaL_addstring; module.exports.luaL_addvalue = luaL_addvalue; diff --git a/src/llex.js b/src/llex.js index e747fdd..4f5b97e 100644 --- a/src/llex.js +++ b/src/llex.js @@ -467,11 +467,13 @@ const read_string = function(ls, del, seminfo) { if (will === 'read_save') next(ls); - else if (will === 'only_save') { + + if (will === 'read_save' || will === 'only_save') { ls.buff.n -= 1; /* remove '\\' */ save(ls, c); - } else if (will === 'no_save') - break; + } + + break; } default: save_and_next(ls); diff --git a/src/lobject.js b/src/lobject.js index 556da08..719d7a6 100644 --- a/src/lobject.js +++ b/src/lobject.js @@ -360,7 +360,7 @@ const l_str2int = function(s) { s = s.slice(2); /* skip '0x' */ for (; ljstype.lisxdigit(s[0]); s = s.slice(1)) { - a = a * 16 + luaO_hexavalue(s); + a = a * 16 + luaO_hexavalue(s[0]); empty = false; } } else { /* decimal */ diff --git a/src/lstrlib.js b/src/lstrlib.js index 376d379..b3d5aea 100644 --- a/src/lstrlib.js +++ b/src/lstrlib.js @@ -1,10 +1,15 @@ "use strict"; const assert = require('assert'); +const sprintf = require('sprintf'); const lua = require('./lua.js'); const lapi = require('./lapi.js'); const lauxlib = require('./lauxlib.js'); +const luaconf = require('./luaconf.js'); +const CT = lua.constant_types; + +const L_ESC = '%'.charCodeAt(0); /* translate a relative string position: negative means back from end */ const posrelat = function(pos, len) { @@ -30,6 +35,290 @@ const str_char = function(L) { return 1; }; +const SIZELENMOD = luaconf.LUA_NUMBER_FRMLEN.length; + +const L_NBFD = 16; // TODO: arbitrary + +// See: http://croquetweak.blogspot.fr/2014/08/deconstructing-floats-frexp-and-ldexp.html +const frexp = function(value) { + if (value === 0) return [value, 0]; + var data = new DataView(new ArrayBuffer(8)); + data.setFloat64(0, value); + var bits = (data.getUint32(0) >>> 20) & 0x7FF; + if (bits === 0) { // denormal + data.setFloat64(0, value * Math.pow(2, 64)); // exp + 64 + bits = ((data.getUint32(0) >>> 20) & 0x7FF) - 64; + } + var exponent = bits - 1022; + var mantissa = ldexp(value, -exponent); + return [mantissa, exponent]; +}; + +const ldexp = function(mantissa, exponent) { + var steps = Math.min(3, Math.ceil(Math.abs(exponent) / 1023)); + var result = mantissa; + for (var i = 0; i < steps; i++) + result *= Math.pow(2, Math.floor((exponent + i) / steps)); + return result; +}; + +/* +** Add integer part of 'x' to buffer and return new 'x' +*/ +const adddigit = function(buff, n, x) { + let d = Math.floor(x)|0; /* get integer part from 'x' */ + buff[n] = d < 10 ? d + '0'.charCodeAt(0) : d - 10 + 'a'.charCodeAt(0); /* add to buffer */ + return x - d; /* return what is left */ +}; + +const num2straux = function(buff, x) { + /* if 'inf' or 'NaN', format it like '%g' */ + if (x === Infinity || isNaN(x)) + return sprintf(luaconf.LUA_NUMBER_FMT, x).split('').map(e => e.charCodeAt(0)); + else if (x === 0) { /* can be -0... */ + /* create "0" or "-0" followed by exponent */ + return sprintf(luaconf.LUA_NUMBER_FMT + "x0p+0", x).split('').map(e => e.charCodeAt(0)); + } else { + let fe = frexp(x); /* 'x' fraction and exponent */ + let m = fe[0]; + let e = fe[1]; + let n = 0; /* character count */ + if (m < 0) { /* is number negative? */ + buff[n++] = '-'.charCodeAt(0); /* add signal */ + m = -m; /* make it positive */ + } + buff[n++] = '0'.charCodeAt(0); + buff[n++] = 'x'.charCodeAt(0); /* add "0x" */ + m = adddigit(buff, n++, m * (1 << L_NBFD)); /* add first digit */ + e -= L_NBFD; /* this digit goes before the radix point */ + if (m > 0) { /* more digits? */ + buff[n++] = luaconf.lua_getlocaledecpoint().charCodeAt(0); /* add radix point */ + do { /* add as many digits as needed */ + m = adddigit(buff, n++, m * 16); + } while (m > 0); + } + let exp = sprintf("p%+d", e).split('').map(e => e.charCodeAt(0)); + return buff.slice(0, n + 1).concat(exp).concat(buff.slice(n)); + } +}; + +const lua_number2strx = function(L, buff, fmt, x) { + let n = num2straux(buff, x); + if (fmt[SIZELENMOD] === 'A'.charCodeAt(0)) { + for (let i = 0; i < n; i++) + buff[i] = String.fromCharCode(buff[i]).toUpperCase().charCodeAt(0); + } else if (fmt[SIZELENMOD] !== 'a'.charCodeAt(0)) + lauxlib.luaL_error(L, "modifiers for format '%a'/'%A' not implemented"); + return n; +}; + +/* +** Maximum size of each formatted item. This maximum size is produced +** by format('%.99f', -maxfloat), and is equal to 99 + 3 ('-', '.', +** and '\0') + number of decimal digits to represent maxfloat (which +** is maximum exponent + 1). (99+3+1 then rounded to 120 for "extra +** expenses", such as locale-dependent stuff) +*/ +const MAX_ITEM = 120;// TODO: + l_mathlim(MAX_10_EXP); + + +/* valid flags in a format specification */ +const FLAGS = ["-", "+", " ", "#", "0"].map(e => e.charCodeAt(0)); + +/* +** maximum size of each format specification (such as "%-099.99d") +*/ +const MAX_FORMAT = 32; + +const isdigit = e => "0".charCodeAt(0) <= e && e <= "9".charCodeAt(0); + +const iscntrl = e => (0x00 <= e && e <= 0x1f) || e === 0x7f; + +const addquoted = function(b, s) { + lauxlib.luaL_addchar(b, '"'); + let len = s.length; + while (len--) { + if (s[0] === '"'.charCodeAt(0) || s[0] === '\\'.charCodeAt(0) || s[0] === '\n'.charCodeAt(0)) { + lauxlib.luaL_addchar(b, '\\'); + lauxlib.luaL_addchar(b, String.fromCharCode(s[0])); + } else if (iscntrl(s[0])) { + let buff = []; + if (!isdigit(s[1])) + buff = sprintf("\\%d", s[0]).split('').map(e => e.charCodeAt(0)); + else + buff = sprintf("\\%03d", s[0]).split('').map(e => e.charCodeAt(0)); + lauxlib.luaL_addstring(b, buff); + } else + lauxlib.luaL_addchar(b, String.fromCharCode(s[0])); + s = s.slice(1); + } + lauxlib.luaL_addchar(b, '"'); +}; + +/* +** Ensures the 'buff' string uses a dot as the radix character. +*/ +const checkdp = function(buff) { + if (buff.indexOf('.') < 0) { /* no dot? */ + let point = luaconf.lua_getlocaledecpoint().charCodeAt(0); /* try locale point */ + let ppoint = buff.indexOf(point); + if (ppoint) point[ppoint] = '.'; /* change it to a dot */ + } +}; + +const addliteral = function(L, b, arg) { + switch(lapi.lua_type(L, arg)) { + case CT.LUA_TSTRING: { + let s = L.stack[lapi.index2addr_(L, arg)].value; + addquoted(b, s, s.length); + break; + } + case CT.LUA_TNUMBER: { + let buff = lauxlib.luaL_prepbuffsize(b, MAX_ITEM); + if (!lapi.lua_isinteger(L, arg)) { /* float? */ + let n = lapi.lua_tonumber(L, arg); /* write as hexa ('%a') */ + buff.b += lapi.lua_number2strx(L, buff.b, `%${luaconf.LUA_INTEGER_FRMLEN}a`, n); + checkdp(buff.b); /* ensure it uses a dot */ + } else { /* integers */ + let n = lapi.lua_tointeger(L, arg); + let format = (n === Number.MIN_SAFE_INTEGER) ? `0x%${luaconf.LUA_INTEGER_FRMLEN}x` : luaconf.LUA_INTEGER_FMT; + buff.b += sprintf(format, n|0); + } + break; + } + case CT.LUA_TNIL: case CT.LUA_TBOOLEAN: { + lauxlib.luaL_tolstring(L, arg); + lauxlib.luaL_addvalue(b); + break; + } + default: { + lauxlib.luaL_argerror(L, arg, "value has no literal form"); + } + } +}; + +const scanformat = function(L, strfrmt, form) { + let p = strfrmt; + while (p[0] !== 0 && FLAGS.indexOf(p[0]) >= 0) p = p.slice(1); /* skip flags */ + if (strfrmt.length - p.length >= FLAGS.length) + lauxlib.luaL_error(L, "invalid format (repeated flags)"); + if (isdigit(p[0])) p = p.slice(1); /* skip width */ + if (isdigit(p[0])) p = p.slice(1); /* (2 digits at most) */ + if (p[0] === '.'.charCodeAt(0)) { + p = p.slice(1); + if (isdigit(p[0])) p = p.slice(1); /* skip precision */ + if (isdigit(p[0])) p = p.slice(1); /* (2 digits at most) */ + } + if (isdigit(p[0])) + lauxlib.luaL_error(L, "invalid format (width or precision too long)"); + form[0] = "%".charCodeAt(0); + for (let i = 0; i < strfrmt.length - p.length + 1; i++) + form[i + 1] = strfrmt[i]; + // form[strfrmt.length - p.length + 2] = 0; + return { + form: form, + p: p + }; +}; + +/* +** add length modifier into formats +*/ +const addlenmod = function(form, lenmod) { + let l = form.length; + let lm = lenmod.length; + let spec = form[l - 1]; + for (let i = 0; i < lenmod.length; i++) + form[i + l - 1] = lenmod[i]; + form[l + lm - 1] = spec; + // form[l + lm] = 0; + return form; +}; + +const str_format = function(L) { + let top = lapi.lua_gettop(L); + let arg = 1; + let strfrmt = lauxlib.luaL_checkstring(L, arg); + strfrmt = L.stack[lapi.index2addr_(L, 1)].value; + let b = new lauxlib.luaL_Buffer(L); + + while (strfrmt.length > 0) { + if (strfrmt[0] !== L_ESC) { + lauxlib.luaL_addchar(b, String.fromCharCode(strfrmt[0])); + strfrmt = strfrmt.slice(1); + } else if ((strfrmt = strfrmt.slice(1))[0] === L_ESC) { + lauxlib.luaL_addchar(b, String.fromCharCode(strfrmt[0])); + strfrmt = strfrmt.slice(1); + } else { /* format item */ + let form = []; /* to store the format ('%...') */ + let buff = lauxlib.luaL_prepbuffsize(b, MAX_ITEM); /* to put formatted item */ + if (++arg > top) + lauxlib.luaL_argerror(L, arg, "no value"); + let f = scanformat(L, strfrmt, form); + strfrmt = f.p; + form = f.form; + switch (String.fromCharCode(strfrmt[0])) { + case 'c': { + strfrmt = strfrmt.slice(1); + buff.b += sprintf(String.fromCharCode(...form), lauxlib.luaL_checkinteger(L, arg)); + break; + } + case 'd': case 'i': + case 'o': case 'u': case 'x': case 'X': { + strfrmt = strfrmt.slice(1); + let n = lauxlib.luaL_checkinteger(L, arg); + form = addlenmod(form, luaconf.LUA_INTEGER_FRMLEN.split('').map(e => e.charCodeAt(0))); + buff.b += sprintf(String.fromCharCode(...form), n|0); + break; + } + case 'a': case 'A': { + strfrmt = strfrmt.slice(1); + form = addlenmod(form, luaconf.LUA_INTEGER_FRMLEN.split('').map(e => e.charCodeAt(0))); + buff.b += lua_number2strx(L, buff.b, form, lauxlib.luaL_checknumber(L, arg)); + break; + } + case 'e': case 'E': case 'f': + case 'g': case 'G': { + strfrmt = strfrmt.slice(1); + let n = lauxlib.luaL_checknumber(L, arg); + form = addlenmod(form, luaconf.LUA_INTEGER_FRMLEN.split('').map(e => e.charCodeAt(0))); + buff.b += sprintf(String.fromCharCode(...form), n); + break; + } + case 'q': { + strfrmt = strfrmt.slice(1); + addliteral(L, b, arg); + break; + } + case 's': { + strfrmt = strfrmt.slice(1); + let str = lauxlib.luaL_tolstring(L, arg); + let s = L.stack[lapi.index2addr_(L, arg)].value; + if (form[2] === '\0') /* no modifiers? */ + lauxlib.luaL_addvalue(b); /* keep entire string */ + else { + lauxlib.luaL_argcheck(L, s.length === str.length, arg, "string contains zeros"); + if (form.indexOf('.') < 0 && s.length >= 100) { + /* no precision and string is too long to be formatted */ + lauxlib.luaL_addvalue(b); /* keep entire string */ + } else { /* format the string into 'buff' */ + buff.b += sprintf(String.fromCharCode(...form), s); + lapi.lua_pop(L, 1); /* remove result from 'luaL_tolstring' */ + } + } + break; + } + default: { /* also treat cases 'pnLlh' */ + return lauxlib.luaL_error(L, `invalid option '%${strfrmt[0]}'`); + } + } + } + } + + lauxlib.luaL_pushresult(b); + return 1; +}; + const str_reverse = function(L) { lapi.lua_pushstring(L, lauxlib.luaL_checkstring(L, 1).split("").reverse().join("")); return 1; @@ -77,6 +366,7 @@ const str_byte = function(L) { const strlib = { "byte": str_byte, "char": str_char, + "format": str_format, "len": str_len, "lower": str_lower, "rep": str_rep, diff --git a/src/ltablib.js b/src/ltablib.js index a7963d3..10d66eb 100644 --- a/src/ltablib.js +++ b/src/ltablib.js @@ -139,7 +139,7 @@ const tconcat = function(L) { let i = lauxlib.luaL_optinteger(L, 3, 1); last = lauxlib.luaL_optinteger(L, 4, last); - let b = new lauxlib.luaL_Buffer(); + let b = new lauxlib.luaL_Buffer(L); lauxlib.luaL_buffinit(L, b); for (; i < last; i++) { diff --git a/src/luaconf.js b/src/luaconf.js index f0066de..fc96224 100644 --- a/src/luaconf.js +++ b/src/luaconf.js @@ -20,6 +20,21 @@ const lua_numbertointeger = function(n) { return n|0; }; -module.exports.LUAI_MAXSTACK = LUAI_MAXSTACK; -module.exports.LUA_IDSIZE = LUA_IDSIZE; -module.exports.lua_numbertointeger = lua_numbertointeger; \ No newline at end of file +const LUA_INTEGER_FRMLEN = ""; +const LUA_NUMBER_FRMLEN = ""; + +const LUA_INTEGER_FMT = `%${LUA_INTEGER_FRMLEN}d`; +const LUA_NUMBER_FMT = "%.7g"; + +const lua_getlocaledecpoint = function() { + return (1.1).toLocaleString().substring(1, 2); +}; + +module.exports.LUAI_MAXSTACK = LUAI_MAXSTACK; +module.exports.LUA_IDSIZE = LUA_IDSIZE; +module.exports.LUA_INTEGER_FMT = LUA_INTEGER_FMT; +module.exports.LUA_INTEGER_FRMLEN = LUA_INTEGER_FRMLEN; +module.exports.LUA_NUMBER_FMT = LUA_NUMBER_FMT; +module.exports.LUA_NUMBER_FRMLEN = LUA_NUMBER_FRMLEN; +module.exports.lua_getlocaledecpoint = lua_getlocaledecpoint; +module.exports.lua_numbertointeger = lua_numbertointeger; \ No newline at end of file diff --git a/src/lutf8lib.js b/src/lutf8lib.js index a1ce6bb..053fb56 100644 --- a/src/lutf8lib.js +++ b/src/lutf8lib.js @@ -112,7 +112,7 @@ const utfchar = function(L) { if (n === 1) /* optimize common case of single char */ pushutfchar(L, 1); else { - let b = new lauxlib.luaL_Buffer(); + let b = new lauxlib.luaL_Buffer(L); lauxlib.luaL_buffinit(L, b); for (let i = 1; i <= n; i++) { pushutfchar(L, i); diff --git a/tests/lstrlib.js b/tests/lstrlib.js index b15eecf..0475a42 100644 --- a/tests/lstrlib.js +++ b/tests/lstrlib.js @@ -220,4 +220,96 @@ test('string.byte', function (t) { 108, "Correct element(s) on the stack" ); +}); + + +test('string.format', function (t) { + let luaCode = ` + return string.format("%%%d %010d", 10, 23) + `, L; + + t.plan(3); + + t.doesNotThrow(function () { + + L = lauxlib.luaL_newstate(); + + linit.luaL_openlibs(L); + + lauxlib.luaL_loadstring(L, luaCode); + + }, "Lua program loaded without error"); + + t.doesNotThrow(function () { + + lapi.lua_call(L, 0, -1); + + }, "Lua program ran without error"); + + t.strictEqual( + lapi.lua_tostring(L, -1), + "%10 0000000023", + "Correct element(s) on the stack" + ); +}); + + +test('string.format', function (t) { + let luaCode = ` + return string.format("%07X", 0xFFFFFFF) + `, L; + + t.plan(3); + + t.doesNotThrow(function () { + + L = lauxlib.luaL_newstate(); + + linit.luaL_openlibs(L); + + lauxlib.luaL_loadstring(L, luaCode); + + }, "Lua program loaded without error"); + + t.doesNotThrow(function () { + + lapi.lua_call(L, 0, -1); + + }, "Lua program ran without error"); + + t.strictEqual( + lapi.lua_tostring(L, -1), + "FFFFFFF", + "Correct element(s) on the stack" + ); +}); + +test('string.format', function (t) { + let luaCode = ` + return string.format("%q", 'a string with "quotes" and \\n new line') + `, L; + + t.plan(3); + + t.doesNotThrow(function () { + + L = lauxlib.luaL_newstate(); + + linit.luaL_openlibs(L); + + lauxlib.luaL_loadstring(L, luaCode); + + }, "Lua program loaded without error"); + + t.doesNotThrow(function () { + + lapi.lua_call(L, 0, -1); + + }, "Lua program ran without error"); + + t.strictEqual( + lapi.lua_tostring(L, -1), + '"a string with \\"quotes\\" and \\\n new line"', + "Correct element(s) on the stack" + ); }); \ No newline at end of file -- cgit v1.2.3-54-g00ecf