123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472 |
- --
- -- Copyright 2019 The FATE Authors. All Rights Reserved.
- --
- -- Licensed under the Apache License, Version 2.0 (the "License");
- -- you may not use this file except in compliance with the License.
- -- You may obtain a copy of the License at
- --
- -- http://www.apache.org/licenses/LICENSE-2.0
- --
- -- Unless required by applicable law or agreed to in writing, software
- -- distributed under the License is distributed on an "AS IS" BASIS,
- -- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- -- See the License for the specific language governing permissions and
- -- limitations under the License.
- --
- local schar = string.char
- local ssub, gsub = string.sub, string.gsub
- local sfind, smatch = string.find, string.match
- local tinsert, tremove = table.insert, table.remove
- local UNESCAPES = {
- ['0'] = "\x00", z = "\x00", N = "\x85",
- a = "\x07", b = "\x08", t = "\x09",
- n = "\x0a", v = "\x0b", f = "\x0c",
- r = "\x0d", e = "\x1b", ['\\'] = '\\',
- }
- -- help function
- local function select(list, pred)
- local selected = {}
- for i = 0, #list do
- local v = list[i]
- if v and pred(v, i) then
- tinsert(selected, v)
- end
- end
- return selected
- end
- -- return: indent_count, left_string
- local function count_indent(line)
- local _, j = sfind(line, '^%s+')
- if not j then
- return 0, line
- end
- return j, ssub(line, j+1)
- end
- local function trim(str)
- return string.gsub(str, "^%s*(.-)%s*$", "%1")
- end
- local function ltrim(str)
- return smatch(str, "^%s*(.-)$")
- end
- local function rtrim(str)
- return smatch(str, "^(.-)%s*$")
- end
- local function isemptyline(line)
- return line == '' or sfind(line, '^%s*$') or sfind(line, '^%s*#')
- end
- local function startswith(haystack, needle)
- return ssub(haystack, 1, #needle) == needle
- end
- local function startswithline(line, needle)
- return startswith(line, needle) and isemptyline(ssub(line, #needle+1))
- end
- -- class
- local class = {__meta={}}
- function class.__meta.__call(cls, ...)
- local self = setmetatable({}, cls)
- if cls.__init then
- cls.__init(self, ...)
- end
- return self
- end
- function class.def(base, type, cls)
- base = base or class
- local mt = {__metatable=base, __index=base}
- for k, v in pairs(base.__meta) do mt[k] = v end
- cls = setmetatable(cls or {}, mt)
- cls.__index = cls
- cls.__metatable = cls
- cls.__type = type
- cls.__meta = mt
- return cls
- end
- local types = {
- null = class:def('null'),
- map = class:def('map'),
- seq = class:def('seq'),
- }
- local Null = types.null
- function Null.__tostring() return 'yaml.null' end
- function Null.isnull(v)
- if v == nil then return true end
- if type(v) == 'table' and getmetatable(v) == Null then return true end
- return false
- end
- local null = Null()
- -- implement function
- local function parse_string(line, stopper)
- stopper = stopper or ''
- local q = ssub(line, 1, 1)
- if q == ' ' or q == '\t' then
- return parse_string(ssub(line, 2))
- end
- if q == "'" then
- local i = sfind(line, "'", 2, true)
- if not i then
- return nil, line
- end
- return ssub(line, 2, i-1), ssub(line, i+1)
- end
- if q == '"' then
- local i, buf = 2, ''
- while i < #line do
- local c = ssub(line, i, i)
- if c == '\\' then
- local n = ssub(line, i+1, i+1)
- if UNESCAPES[n] ~= nil then
- buf = buf..UNESCAPES[n]
- elseif n == 'x' then
- local h = ssub(i+2,i+3)
- if sfind(h, '^[0-9a-fA-F]$') then
- buf = buf..schar(tonumber(h, 16))
- i = i + 2
- else
- buf = buf..'x'
- end
- else
- buf = buf..n
- end
- i = i + 1
- elseif c == q then
- break
- else
- buf = buf..c
- end
- i = i + 1
- end
- return buf, ssub(line, i+1)
- end
- if q == '-' or q == ':' then
- if ssub(line, 2, 2) == ' ' or #line == 1 then
- return nil, line
- end
- end
- local buf = ''
- while #line > 0 do
- local c = ssub(line, 1, 1)
- if sfind(stopper, c, 1, true) then
- break
- elseif c == ':' and (ssub(line, 2, 2) == ' ' or #line == 1) then
- break
- elseif c == '#' and (ssub(buf, #buf, #buf) == ' ') then
- break
- else
- buf = buf..c
- end
- line = ssub(line, 2)
- end
- return rtrim(buf), line
- end
- local function parse_flowstyle(line, lines)
- local stack = {}
- while true do
- if #line == 0 then
- if #lines == 0 then
- break
- else
- line = tremove(lines, 1)
- end
- end
- local c = ssub(line, 1, 1)
- if c == '#' then
- line = ''
- elseif c == ' ' or c == '\t' or c == '\r' or c == '\n' then
- line = ssub(line, 2)
- elseif c == '{' or c == '[' then
- tinsert(stack, {v={},t=c})
- line = ssub(line, 2)
- elseif c == ':' then
- local s = tremove(stack)
- tinsert(stack, {v=s.v, t=':'})
- line = ssub(line, 2)
- elseif c == ',' then
- local value = tremove(stack)
- if value.t == ':' or value.t == '{' or value.t == '[' then error() end
- if stack[#stack].t == ':' then
- -- map
- local key = tremove(stack)
- stack[#stack].v[key.v] = value.v
- elseif stack[#stack].t == '{' then
- -- set
- stack[#stack].v[value.v] = true
- elseif stack[#stack].t == '[' then
- -- seq
- tinsert(stack[#stack].v, value.v)
- end
- line = ssub(line, 2)
- elseif c == '}' then
- if stack[#stack].t == '{' then
- if #stack == 1 then break end
- stack[#stack].t = '}'
- line = ssub(line, 2)
- else
- line = ','..line
- end
- elseif c == ']' then
- if stack[#stack].t == '[' then
- if #stack == 1 then break end
- stack[#stack].t = ']'
- line = ssub(line, 2)
- else
- line = ','..line
- end
- else
- local s, rest = parse_string(line, ',{}[]')
- if not s then
- error('invalid flowstyle line: '..line)
- end
- tinsert(stack, {v=s, t='s'})
- line = rest
- end
- end
- return stack[1].v, line
- end
- local function parse_scalar(line, lines)
- line = ltrim(line)
- line = gsub(line, '%s*#.*$', '')
-
- if line == '' or line == '~' then
- return null
- end
-
- if startswith(line, '{') or startswith(line, '[') then
- return parse_flowstyle(line, lines)
- end
- local s, _ = parse_string(line)
- if s and s ~= line then
- return s
- end
- -- Special cases
- if sfind('\'"!$', ssub(line, 1, 1), 1, true) then
- error('unsupported line: '..line)
- end
-
- if startswithline(line, '{}') then
- return {}
- end
- if startswithline(line, '[]') then
- return {}
- end
-
- -- Regular unquoted string
- local v = line
- if v == 'null' or v == 'Null' or v == 'NULL'then
- return null
- elseif v == 'true' or v == 'True' or v == 'TRUE' then
- return true
- elseif v == 'false' or v == 'False' or v == 'FALSE' then
- return false
- elseif v == '.inf' or v == '.Inf' or v == '.INF' then
- return math.huge
- elseif v == '+.inf' or v == '+.Inf' or v == '+.INF' then
- return math.huge
- elseif v == '-.inf' or v == '-.Inf' or v == '-.INF' then
- return -math.huge
- elseif v == '.nan' or v == '.NaN' or v == '.NAN' then
- return 0 / 0
- elseif sfind(v, '^[%+%-]?[0-9]+$') or sfind(v, '^[%+%-]?[0-9]+%.$')then
- return tonumber(v)
- elseif sfind(v, '^[%+%-]?[0-9]+%.[0-9]+$') then
- return tonumber(v)
- end
- return v
- end
- local parse_map
- local function parse_seq(line, lines, indent)
- local seq = setmetatable({}, types.seq)
- if line ~= '' then
- error()
- end
- while #lines > 0 do
- line = lines[1]
- local level = count_indent(line)
- if level < indent and indent ~= -1 then
- return seq
- elseif level > indent and indent ~= -1 then
- error("found bad indenting in line: ".. line)
- end
- local i, j = sfind(line, '%-%s+')
- if not i then
- i, j = sfind(line, '%-$')
- if not i then
- return seq
- end
- end
- local rest = ssub(line, j+1)
- if sfind(rest, '^[^\'\"%s]*:') then
- local indent2 = j
- lines[1] = string.rep(' ', indent2)..rest
- tinsert(seq, parse_map('', lines, indent2))
- elseif isemptyline(rest) then
- tremove(lines, 1)
- if #lines == 0 then
- tinsert(seq, null)
- return seq
- end
- if sfind(lines[1], '^%s*%-') then
- local nextline = lines[1]
- local indent2 = count_indent(nextline)
- if indent2 == indent then
- tinsert(seq, null)
- else
- tinsert(seq, parse_seq('', lines, indent2))
- end
- else
- local nextline = lines[1]
- local indent2 = count_indent(nextline)
- tinsert(seq, parse_map('', lines, indent2))
- end
- elseif rest then
- tremove(lines, 1)
- local tmp = parse_scalar(rest, lines)
- tinsert(seq, tmp)
- end
- end
- return seq
- end
- function parse_map(line, lines, indent)
- if not isemptyline(line) then
- error('not map line: '..line)
- end
- local map = setmetatable({}, types.map)
- while #lines > 0 do
- line = lines[1]
-
- local level, _ = count_indent(line)
- if level < indent then
- return map
- elseif level > indent then
- error("found bad indenting in line: ".. line)
- end
- local key
- local s, rest = parse_string(line)
- if s and startswith(rest, ':') then
- local sc = parse_scalar(s, {})
- if sc and type(sc) ~= 'string' then
- key = sc
- else
- key = s
- end
- line = ssub(rest, 2)
- else
- error("failed to classify line: "..line)
- end
- if map[key] ~= nil then
- print("found a duplicate key '"..key.."' in line: "..line)
- local suffix = 1
- while map[key..'__'..suffix] do
- suffix = suffix + 1
- end
- key = key ..'_'..suffix
- end
- line = ltrim(line)
- if not isemptyline(line) then
- tremove(lines, 1)
- line = ltrim(line)
- map[key] = parse_scalar(line, lines)
- else
- tremove(lines, 1)
- if #lines == 0 then
- map[key] = null
- return map;
- end
- if sfind(lines[1], '^%s*%-') then
- local indent2 = count_indent(lines[1])
- map[key] = parse_seq('', lines, indent2)
- else
- local indent2 = count_indent(lines[1])
- if indent >= indent2 then
- map[key] = null
- else
- map[key] = parse_map('', lines, indent2)
- end
- end
- end
- end
- return map
- end
- local function parse_documents(lines)
- lines = select(lines, function(s) return not isemptyline(s) end)
- if #lines == 1 and not sfind(lines[1], '^%s*%-') then
- local line = lines[1]
- line = ltrim(line)
- return parse_scalar(line, lines)
- end
- local root = {}
- while #lines > 0 do
- local line = lines[1]
- if sfind(line, '^%s*%-') then
- tinsert(root, parse_seq('', lines, -1))
- elseif sfind(line, '^%s*[^%s]') then
- local level = count_indent(line)
- tinsert(root, parse_map('', lines, level))
- else
- error('parse error: '..line)
- end
- end
- if #root > 1 and Null.isnull(root[1]) then
- tremove(root, 1)
- return root
- end
- return root
- end
- local function parse(yaml)
- local lines = {}
- for line in string.gmatch(yaml..'\n', '(.-)\n') do
- table.insert(lines, line)
- end
- local docs = parse_documents(lines)
- if #docs == 1 then
- return docs[1]
- end
- return docs
- end
- return {
- null = null,
- parse = parse,
- }
|