123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948 |
- local TOML = {
- -- denotes the current supported TOML version
- version = 0.40,
- -- sets whether the parser should follow the TOML spec strictly
- -- currently, no errors are thrown for the following rules if strictness is turned off:
- -- tables having mixed keys
- -- redefining a table
- -- redefining a key within a table
- strict = true,
- }
- local date_metatable = {
- __tostring = function( t )
- local rep = ''
- if t.year then
- rep = rep .. string.format("%04d-%02d-%02d", t.year, t.month, t.day)
- end
- if t.hour then
- if t.year then
- rep = rep .. ' '
- end
- rep = rep .. string.format("%02d:%02d:", t.hour, t.min)
- local sec, frac = math.modf( t.sec )
- rep = rep .. string.format("%02d", sec)
- if frac > 0 then
- rep = rep .. tostring(frac):gsub("0(.-)0*$","%1")
- end
- end
- if t.zone then
- if t.zone >= 0 then
- rep = rep .. '+' .. string.format("%02d:00", t.zone)
- elseif t.zone < 0 then
- rep = rep .. '-' .. string.format("%02d:00", -t.zone)
- end
- end
- return rep
- end,
- }
- local setmetatable, getmetatable = setmetatable, getmetatable
- TOML.datefy = function( tab )
- -- TODO : VALIDATE !
- return setmetatable(tab, date_metatable)
- end
- TOML.isdate = function( tab )
- return getmetatable( tab ) == date_metatable
- end
- -- converts TOML data into a lua table
- TOML.multistep_parser = function (options)
- options = options or {}
- local strict = (options.strict ~= nil and options.strict or TOML.strict)
- local toml = ''
- -- the output table
- local out = {}
- local ERR = {}
- -- the current table to write to
- local obj = out
- -- stores text data
- local buffer = ""
- -- the current location within the string to parse
- local cursor = 1
- -- remember that the last chunk was already read
- local stream_ended = false
- local nl_count = 1
- local function result_or_error()
- if #ERR > 0 then return nil, table.concat(ERR) end
- return out
- end
- -- produce a parsing error message
- -- the error contains the line number of the current position
- local function err(message, strictOnly)
- if not strictOnly or (strictOnly and strict) then
- local line = 1
- local c = 0
- local msg = "At TOML line " .. nl_count .. ': ' .. message .. "."
- if not ERR[msg] then
- ERR[1+#ERR] = msg
- ERR[msg] = true
- end
- end
- end
- -- read n characters (at least) or chunk terminator (nil)
- local function getNewData(n)
- while not stream_ended do
- if cursor + (n or 0) < #toml then break end
- local new_data = coroutine.yield(result_or_error())
- if new_data == nil then
- stream_ended = true
- break
- end
- toml = toml:sub(cursor)
- cursor = 1
- toml = toml .. new_data
- end
- end
- -- TODO : use 1-based indexing ?
- -- returns the next n characters from the current position
- local function getData( a, b )
- getNewData(b)
- a = a or 0
- b = b or (toml:len() - cursor)
- return toml:sub( cursor + a, cursor + b )
- end
- -- count how many new lines are in the next n chars
- local function count_source_line(n)
- local count = 0
- for _ in getData(0, n-1):gmatch('\n') do
- count = count + 1
- end
- return count
- end
- -- moves the current position forward n (default: 1) characters
- local function step(n)
- n = n or 1
- nl_count = nl_count + count_source_line(n)
- cursor = cursor + n
- end
- -- prevent infinite loops by checking whether the cursor is
- -- at the end of the document or not
- local function bounds()
- if cursor <= toml:len() then return true end
- getNewData(1)
- return cursor <= toml:len()
- end
- -- Check if we are at end of the data
- local function dataEnd()
- return cursor >= toml:len()
- end
- -- returns the next n characters from the current position
- local function char(n)
- n = n or 0
- return getData(n, n)
- end
- -- Match official TOML definition of whitespace
- local function matchWs(n)
- n = n or 0
- return getData(n,n):match("[\009\032]")
- end
- -- Match the official TOML definition of newline
- local function matchnl(n)
- n = n or 0
- local c = getData(n,n)
- if c == '\10' then return '\10' end
- return getData(n,n+1):match("^\13\10")
- end
- -- move forward until the next non-whitespace character
- local function skipWhitespace()
- while(matchWs()) do
- step()
- end
- end
- -- remove the (Lua) whitespace at the beginning and end of a string
- local function trim(str)
- return str:gsub("^%s*(.-)%s*$", "%1")
- end
- -- divide a string into a table around a delimiter
- local function split(str, delim)
- if str == "" then return {} end
- local result = {}
- local append = delim
- if delim:match("%%") then
- append = delim:gsub("%%", "")
- end
- for match in (str .. append):gmatch("(.-)" .. delim) do
- table.insert(result, match)
- end
- return result
- end
- local function parseString()
- local quoteType = char() -- should be single or double quote
- -- this is a multiline string if the next 2 characters match
- local multiline = (char(1) == char(2) and char(1) == char())
- -- buffer to hold the string
- local str = ""
- -- skip the quotes
- step(multiline and 3 or 1)
- while(bounds()) do
- if multiline and matchnl() and str == "" then
- -- skip line break line at the beginning of multiline string
- step()
- end
- -- keep going until we encounter the quote character again
- if char() == quoteType then
- if multiline then
- if char(1) == char(2) and char(1) == quoteType then
- step(3)
- break
- end
- else
- step()
- break
- end
- end
- if matchnl() and not multiline then
- err("Single-line string cannot contain line break")
- end
- -- if we're in a double-quoted string, watch for escape characters!
- if quoteType == '"' and char() == "\\" then
- if multiline and matchnl(1) then
- -- skip until first non-whitespace character
- step(1) -- go past the line break
- while(bounds()) do
- if not matchWs() and not matchnl() then
- break
- end
- step()
- end
- else
- -- all available escape characters
- local escape = {
- b = "\b",
- t = "\t",
- n = "\n",
- f = "\f",
- r = "\r",
- ['"'] = '"',
- ["\\"] = "\\",
- }
- -- utf function from http://stackoverflow.com/a/26071044
- -- converts \uXXX into actual unicode
- local function utf(char)
- local bytemarkers = {{0x7ff, 192}, {0xffff, 224}, {0x1fffff, 240}}
- if char < 128 then return string.char(char) end
- local charbytes = {}
- for bytes, vals in pairs(bytemarkers) do
- if char <= vals[1] then
- for b = bytes + 1, 2, -1 do
- local mod = char % 64
- char = (char - mod) / 64
- charbytes[b] = string.char(128 + mod)
- end
- charbytes[1] = string.char(vals[2] + char)
- break
- end
- end
- return table.concat(charbytes)
- end
- if escape[char(1)] then
- -- normal escape
- str = str .. escape[char(1)]
- step(2) -- go past backslash and the character
- elseif char(1) == "u" then
- -- utf-16
- step()
- local uni = char(1) .. char(2) .. char(3) .. char(4)
- step(5)
- uni = tonumber(uni, 16)
- if not uni then
- err("Unicode escape is not a Unicode scalar")
- elseif (uni >= 0 and uni <= 0xd7ff) and not (uni >= 0xe000 and uni <= 0x10ffff) then
- str = str .. utf(uni)
- else
- err("Unicode escape is not a Unicode scalar")
- end
- elseif char(1) == "U" then
- -- utf-32
- step()
- local uni = char(1) .. char(2) .. char(3) .. char(4) .. char(5) .. char(6) .. char(7) .. char(8)
- step(9)
- uni = tonumber(uni, 16)
- if (uni >= 0 and uni <= 0xd7ff) and not (uni >= 0xe000 and uni <= 0x10ffff) then
- str = str .. utf(uni)
- else
- err("Unicode escape is not a Unicode scalar")
- end
- else
- err("Invalid escape")
- step()
- end
- end
- else
- -- if we're not in a double-quoted string, just append it to our buffer raw and keep going
- str = str .. char()
- step()
- end
- end
- return {value = str, type = "string"}
- end
- local function matchDate()
- local year, month, day, n =
- getData(0, 10):match('^(%d%d%d%d)%-([0-1][0-9])%-([0-3][0-9])()')
- if not year then return nil end
- step(n-1)
- return year, month, day
- end
- local function matchTime()
- local hour, minute, second, n =
- getData(0, 19):match('^([0-2][0-9])%:([0-6][0-9])%:(%d+%.?%d*)()')
- if not hour then return nil end
- step(n-1)
- return hour, minute, second
- end
- local function matchTimezone()
- local eastwest, offset, zero, n =
- getData(0, 6):match('^([%+%-])([0-9][0-9])%:([0-9][0-9])()')
- if not eastwest then return nil end
- step(n-1)
- return eastwest .. offset
- end
- local function parseDate()
- local year, month, day = matchDate()
- if not year then err("Invalid date") end
- local hour, minute, second = '', '', '', ''
- local date_time_separator = false
- if char():match('[T ]') then
- step(1)
- date_time_separator = true
- end
- local n
- if date_time_separator then
- hour, minute, second, n = matchTime()
- if not hour then err("Invalid date") end
- end
- local zone
- if char():match('Z') then
- step(1)
- zone = 0
- else
- local timezone = matchTimezone()
- if timezone then
- zone = tonumber(timezone)
- end
- end
- local value = {
- year = tonumber(year),
- month = tonumber(month),
- day = tonumber(day),
- hour = tonumber(hour),
- min = tonumber(minute),
- sec = tonumber(second),
- zone = zone,
- }
- local e
- value, e = TOML.datefy(value)
- if not value then
- err(e)
- end
- return {
- type = "date",
- value = value,
- }
- end
- local function parseTime()
- local hour, minute, second, n = matchTime()
- if not hour then err("Invalid date") end
- local value = {
- hour = tonumber(hour),
- min = tonumber(minute),
- sec = tonumber(second),
- }
- local value, e = TOML.datefy(value)
- if not value then err(e) end
- return {
- type = "date",
- value = value,
- }
- end
- local function parseNumber()
- local num = ""
- local exp
- local date = false
- local dotfound = false
- local prev_underscore = false
- while(bounds()) do
- if char():match("[%+%-%.eE_0-9]") then
- if char():match'%.' then dotfound = true end
- if not exp then
- if char():lower() == "e" then
- -- as soon as we reach e or E, start appending to exponent buffer instead of
- -- number buffer
- exp = ""
- elseif char() ~= "_" then
- num = num .. char()
- end
- elseif char():match("[%+%-_0-9]") then
- if char() ~= "_" then
- exp = exp .. char()
- end
- else
- err("Invalid exponent")
- end
- elseif matchWs() or char() == "#" or matchnl() or char() == "," or char() == "]" or char() == "}" then
- break
- else
- err("Invalid number")
- end
- if char() == '_' and num:sub(#num) == '.' then
- err('Undescore after decimal point')
- end
- if char() == '_' and char(1) == '.' then
- err('Undescore before decimal point')
- end
- if char() == '_' and prev_underscore then
- err('Double underscore in number')
- end
- if char() == "_" then
- prev_underscore = true
- else
- prev_underscore = false
- end
- step()
- end
- if prev_underscore then
- err("Invalid undescore at end of number")
- end
- if date then
- return {value = num, type = "date"}
- end
- if num:match('^[%+%-]?0[0-9]') then
- err('Leading zero found in number')
- end
- if dotfound then
- if num:match('%.$') then
- err('No trailing zero found in float')
- end
- end
- exp = exp and tonumber(exp) or 0
- if exp > 0 then
- exp = math.floor(10 ^ exp)
- elseif exp < 0 then
- exp = 10 ^ exp
- elseif exp == 0 then
- exp = 1
- end
- num = tonumber(num) * exp
- if exp < 0 or dotfound then
- return {value = num, type = "float"}
- end
- return {value = num, type = "integer"}
- end
- local parseArray, getValue
-
- function parseArray()
- step() -- skip [
- skipWhitespace()
- local arrayType
- local array = {}
- while(bounds()) do
- if char() == "]" then
- break
- elseif matchnl() then
- -- skip
- step()
- skipWhitespace()
- elseif char() == "#" then
- while(bounds() and not matchnl()) do
- step()
- end
- else
- -- get the next object in the array
- local v = getValue()
- if not v then break end
- -- set the type if it hasn't been set before
- if arrayType == nil then
- arrayType = v.type
- elseif arrayType ~= v.type then
- err("Mixed types in array", true)
- end
- array = array or {}
- table.insert(array, v.value)
-
- if char() == "," then
- step()
- end
- skipWhitespace()
- end
- end
- step()
- return {value = array, type = "array"}
- end
- local function parseInlineTable()
- step() -- skip opening brace
- local buffer = ""
- local quoted = false
- local tbl = {}
- while bounds() do
- if char() == "}" then
- break
- elseif char() == "'" or char() == '"' then
- buffer = parseString().value
- quoted = true
- skipWhitespace()
- elseif char() == "=" then
- if not quoted then
- buffer = trim(buffer)
- end
- step() -- skip =
- skipWhitespace()
- if matchnl() then
- err("Newline in inline table")
- end
- local v = getValue().value
- tbl[buffer] = v
- skipWhitespace()
- if char() == "," then
- step()
- elseif matchnl() then
- err("Newline in inline table")
- end
- quoted = false
- buffer = ""
- else
- if quoted then
- if not matchWs() then
- err("Unexpected character after the key")
- end
- else
- buffer = buffer .. char()
- end
- step()
- end
- end
- step() -- skip closing brace
- return {value = tbl, type = "array"}
- end
- local function parseBoolean()
- local v
- if getData(0, 3) == "true" then
- step(4)
- v = {value = true, type = "boolean"}
- elseif getData(0, 4) == "false" then
- step(5)
- v = {value = false, type = "boolean"}
- else
- err("Invalid primitive")
- end
- skipWhitespace()
- if char() == "#" then
- while(not matchnl()) do
- step()
- end
- end
- return v
- end
- -- figure out the type and get the next value in the document
- function getValue()
- if char() == '"' or char() == "'" then
- return parseString()
- elseif getData(0,5):match("^%d%d%d%d%-%d") then
- return parseDate()
- elseif getData(0,3):match("^%d%d%:%d") then
- return parseTime()
- elseif char():match("[%+%-0-9]") then
- return parseNumber()
- elseif char() == "[" then
- return parseArray()
- elseif char() == "{" then
- return parseInlineTable()
- else
- return parseBoolean()
- end
- -- date regex (for possible future support):
- -- %d%d%d%d%-[0-1][0-9]%-[0-3][0-9]T[0-2][0-9]%:[0-6][0-9]%:[0-6][0-9][Z%:%+%-%.0-9]*
- end
- local function parse()
- local function check_key()
- if buffer == "" then
- err("Empty key")
- end
- if buffer:match("[%s%c%%%(%)%*%+%.%?%[%]!\"#$&',/:;<=>@`\\^{|}~]") and not quotedKey then
- err('Invalid character in key')
- end
- end
- -- avoid double table definition
- local defined_table = setmetatable({},{__mode='kv'})
- -- keep track of container type i.e. table vs array
- local container_type = setmetatable({},{__mode='kv'})
- local function processKey(isLast, tableArray)
- if isLast and obj[buffer] and not tableArray and #obj[buffer] > 0 then
- err("Cannot redefine table", true)
- end
- -- set obj to the appropriate table so we can start
- -- filling it with values!
- if tableArray then
- -- push onto cache
- local current = obj[buffer]
- -- crete as needed + identify table vs array
- local isArray = false
- if current then
- isArray = (container_type[current] == 'array')
- else
- current = {}
- obj[buffer] = current
- if isLast then
- isArray = true
- container_type[current] = 'array'
- else
- isArray = false
- container_type[current] = 'hash'
- end
- end
- if isLast and not isArray then
- err('The selected key contains a table, not an array', true)
- end
- -- update current object
- if not isLast then obj = current end
- if isArray then
- if isLast then table.insert(current, {}) end
- obj = current[#current]
- end
- else
- local newObj = obj[buffer] or {}
- obj[buffer] = newObj
- if #newObj > 0 then
- if type(newObj) ~= 'table' then
- err('Duplicate field')
- else
- -- an array is already in progress for this key, so modify its
- -- last element, instead of the array itself
- obj = newObj[#newObj]
- end
- else
- obj = newObj
- end
- end
- if isLast then
- if defined_table[obj] then
- err('Duplicated table definition')
- end
- defined_table[obj] = true
- end
- end
- -- track whether the current key was quoted or not
- local quotedKey = false
- -- parse the document!
- while(bounds()) do
-
- -- skip comments and whitespace
- if char() == "#" then
- while(not matchnl()) do
- step()
- end
- end
- if matchnl() then
- if trim(buffer) ~= '' then
- err('Invalid key')
- end
- end
- if char() == "=" then
- step()
- skipWhitespace()
-
- -- trim key name
- buffer = trim(buffer)
- if not quotedKey then check_key() end
- if buffer:match("^[0-9]+$") and not quotedKey then
- buffer = tonumber(buffer)
- end
- if buffer == "" and not quotedKey then
- err("Empty key name")
- end
- local v = getValue()
- if v then
- -- if the key already exists in the current object, throw an error
- if obj[buffer] ~= nil then
- err('Cannot redefine key "' .. buffer .. '"', true)
- end
- obj[buffer] = v.value
- end
- -- clear the buffer
- buffer = ""
- quotedKey = false
- -- skip whitespace and comments
- skipWhitespace()
- if char() == "#" then
- while(bounds() and not matchnl()) do
- step()
- end
- end
- -- if there is anything left on this line after parsing a key and its value,
- -- throw an error
- if not dataEnd() and not matchnl() then
- err("Invalid primitive")
- end
- elseif char() == "[" then
- if trim(buffer) ~= '' then
- err("Invalid key")
- end
- buffer = ""
- step()
- local tableArray = false
- -- if there are two brackets in a row, it's a table array!
- if char() == "[" then
- tableArray = true
- step()
- end
- obj = out
- while(bounds()) do
- if char() == "]" then
- break
- elseif char() == '"' or char() == "'" then
- buffer = parseString().value
- quotedKey = true
- elseif char() == "." then
- step() -- skip period
- buffer = trim(buffer)
- if not quotedKey then check_key() end
- processKey(false, tableArray, quotedKey)
- buffer = ""
- elseif char() == "[" then
- err('Invalid character in key')
- step()
- else
- buffer = buffer .. char()
- step()
- end
- end
- if tableArray then
- if char(1) ~= "]" then
- err("Mismatching brackets")
- else
- step() -- skip inside bracket
- end
- end
- step() -- skip outside bracket
- buffer = trim(buffer)
- if not quotedKey then check_key() end
- processKey(true, tableArray, quotedKey)
- buffer = ""
- buffer = ""
- quotedKey = false
- skipWhitespace()
- if bounds() and (not char():match('#') and not matchnl()) then
- err("Something found on the same line of a table definition")
- end
- elseif (char() == '"' or char() == "'") then
- -- quoted key
- buffer = parseString().value
- quotedKey = true
- else
- buffer = buffer .. (matchnl() and "" or char())
- step()
- end
- end
- return result_or_error()
- end
- local coparse = coroutine.wrap(parse)
- coparse()
- return coparse
- end
- TOML.parse = function(data, options)
- local cp = TOML.multistep_parser(options)
- cp(data)
- return cp()
- end
- TOML.encode = function(tbl)
- local toml = ""
- local cache = {}
- local function parse(tbl)
- for k, v in pairs(tbl) do
- if type(v) == "boolean" then
- toml = toml .. k .. " = " .. tostring(v) .. "\n"
- elseif type(v) == "number" then
- toml = toml .. k .. " = " .. tostring(v) .. "\n"
- elseif type(v) == "string" then
- local quote = '"'
- v = v:gsub("\\", "\\\\")
- -- if the string has any line breaks, make it multiline
- if v:match("^\n(.*)$") then
- quote = quote:rep(3)
- v = "\\n" .. v
- elseif v:match("\n") then
- quote = quote:rep(3)
- end
- v = v:gsub("\b", "\\b")
- v = v:gsub("\t", "\\t")
- v = v:gsub("\f", "\\f")
- v = v:gsub("\r", "\\r")
- v = v:gsub('"', '\\"')
- v = v:gsub("/", "\\/")
- toml = toml .. k .. " = " .. quote .. v .. quote .. "\n"
- elseif type(v) == "table" and getmetatable(v) == date_metatable then
- toml = toml .. k .. " = " .. tostring(v) .. "\n"
- elseif type(v) == "table" then
- local array, arrayTable = true, true
- local first = {}
- for kk, vv in pairs(v) do
- if type(kk) ~= "number" then array = false end
- if type(vv) ~= "table" then
- v[kk] = nil
- first[kk] = vv
- arrayTable = false
- end
- end
- if array then
- if arrayTable then
- -- double bracket syntax go!
- table.insert(cache, k)
- for kk, vv in pairs(v) do
- toml = toml .. "[[" .. table.concat(cache, ".") .. "]]\n"
- for k3, v3 in pairs(vv) do
- if type(v3) ~= "table" then
- vv[k3] = nil
- first[k3] = v3
- end
- end
- parse(first)
- parse(vv)
- end
- table.remove(cache)
- else
- -- plain ol boring array
- toml = toml .. k .. " = [\n"
- local quote = '"'
- for kk, vv in pairs(first) do
- if type(vv) == "string" then
- toml = toml .. quote .. tostring(vv) .. quote .. ",\n"
- else
- toml = toml .. tostring(vv) .. ",\n"
- end
- end
- toml = toml .. "]\n"
- end
- else
- -- just a key/value table, folks
- table.insert(cache, k)
- toml = toml .. "[" .. table.concat(cache, ".") .. "]\n"
- parse(first)
- parse(v)
- table.remove(cache)
- end
- end
- end
- end
-
- parse(tbl)
-
- return toml:sub(1, -2)
- end
- return TOML
|