From 63952430a7ccd88a6eab4721dad57e66eb407fff Mon Sep 17 00:00:00 2001 From: Karl Berry Date: Fri, 24 Sep 2021 21:44:17 +0000 Subject: yamlvars (24sep21) git-svn-id: svn://tug.org/texlive/trunk@60602 c570f23f-e606-0410-a88d-b1316a301751 --- .../texmf-dist/tex/lualatex/yamlvars/YAMLvars.lua | 369 ++++++++++ .../texmf-dist/tex/lualatex/yamlvars/YAMLvars.sty | 59 ++ .../texmf-dist/tex/lualatex/yamlvars/tinyyaml.lua | 816 +++++++++++++++++++++ 3 files changed, 1244 insertions(+) create mode 100644 Master/texmf-dist/tex/lualatex/yamlvars/YAMLvars.lua create mode 100644 Master/texmf-dist/tex/lualatex/yamlvars/YAMLvars.sty create mode 100644 Master/texmf-dist/tex/lualatex/yamlvars/tinyyaml.lua (limited to 'Master/texmf-dist/tex/lualatex') diff --git a/Master/texmf-dist/tex/lualatex/yamlvars/YAMLvars.lua b/Master/texmf-dist/tex/lualatex/yamlvars/YAMLvars.lua new file mode 100644 index 00000000000..11f75edda35 --- /dev/null +++ b/Master/texmf-dist/tex/lualatex/yamlvars/YAMLvars.lua @@ -0,0 +1,369 @@ +--% Kale Ewasiuk (kalekje@gmail.com) +--% 2021-09-24 +--% +--% Copyright (C) 2021 Kale Ewasiuk +--% +--% Permission is hereby granted, free of charge, to any person obtaining a copy +--% of this software and associated documentation files (the "Software"), to deal +--% in the Software without restriction, including without limitation the rights +--% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +--% copies of the Software, and to permit persons to whom the Software is +--% furnished to do so, subject to the following conditions: +--% +--% The above copyright notice and this permission notice shall be included in +--% all copies or substantial portions of the Software. +--% +--% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +--% ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +--% TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +--% PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +--% SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR +--% ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +--% ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +--% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE +--% OR OTHER DEALINGS IN THE SOFTWARE. + + +-- tinyyaml license +--MIT License +-- +--Copyright (c) 2017 peposso +-- +--Permission is hereby granted, free of charge, to any person obtaining a copy +--of this software and associated documentation files (the "Software"), to deal +--in the Software without restriction, including without limitation the rights +--to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +--copies of the Software, and to permit persons to whom the Software is +--furnished to do so, subject to the following conditions: +-- +--The above copyright notice and this permission notice shall be included in all +--copies or substantial portions of the Software. +-- +--THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +--IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +--FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +--AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +--LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +--OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +--SOFTWARE. + + + +YAMLvars = {} -- self table +YAMLvars.xfm = {} +YAMLvars.prc = {} + +YAMLvars.varsvals = {} +YAMLvars.varspecs = {} + +YAMLvars.prcDefault = 'gdef' +YAMLvars.dftDefault = nil +YAMLvars.xfmDefault = {} + +YAMLvars.allowUndeclared = false + +YAMLvars.tabmidrule = 'hline' + +YAMLvars.yaml = require('tinyyaml') + + +-- xfm functions (transforms) -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- +function YAMLvars.xfm.addxspace(var, val) + return val .. '\\xspace' +end + +function YAMLvars.xfm.tab2arr(var, val) + return pl.array2d.from_table(val) +end + +function YAMLvars.xfm.arrsort2ZA(var, val) + return pl.array2d.sortOP(val, pl.operator.strgt) +end + +function YAMLvars.xfm.addrule2arr(var, val) + return pl.array2d.map_slice2(_1..'\\\\\\'.. YAMLvars.tabmidrule..' ', val, 1,-1,-2,-1) -- todo make gmidrule +end + +function YAMLvars.xfm.arr2tabular(var, val) + return pl.array2d.toTeX(val)..'\\\\' +end + + +function YAMLvars.xfm.arr2itemize(var, val) + return pl.List(val):map('\\item '.._1):join(' ') +end + + +function YAMLvars.xfm.arrsortAZ(var, val) + return pl.List(val):sort(pl.operator.strlt) +end + +function YAMLvars.xfm.arrsortZA(var, val) + return pl.List(val):sort(pl.operator.strgt) +end + +local function complastname(a, b) + a = a:split(' ') + b = b:split(' ') + a = a[#a] + b = b[#b] + return a < b +end + +function YAMLvars.xfm.arrsortlastnameAZ(var, val) + val = pl.List(val):sort(complastname) + return val +end + +function YAMLvars.xfm.list2nl(var, val) + val = pl.array2d.map_slice1(_1..'\\\\', val, 1,-2) + return table.concat(val, '') + --return pl.tablex.reduce(_1.._2, val, '') +end + +function YAMLvars.xfm.lb2nl(var, val) + val, _ = val:gsub('\n','\\\\ ') + return val +end + +function YAMLvars.xfm.lowercase(var, val) + return val:lower() +end + + +function YAMLvars.xfm.markdown(var, val) + --return '\\begin{markdown} '..val..'\n \\end{markdown}' + help_wrt(val, md) + return [[begin markdown ..val.. + + par end markdown]] +end + + + +-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- + + +-- prc functions (processing) -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- + +function YAMLvars.prc.gdef(var, val) + token.set_macro(var, val, 'global') +end + +function YAMLvars.prc.yvdef(var, val) + token.set_macro('yv--'..var, val, 'global') +end + +function YAMLvars.prc.PDFtitle(var, val) + tex.print('\\hypersetup{pdftitle={'..val..'}}') + --tex.print('\\setPDFtitle{'..val..'}') +end + +function YAMLvars.prc.PDFauthor(var, val) + tex.print('\\hypersetup{pdfauthor={'..val..'}}') + --tex.print('\\setPDFauthor{'..val..'}') +end + + +function YAMLvars.prc.setheader(val, rl) + local _, count = string.gsub(val, '\\\\', '') + if count == 0 then + val = '{\\ }\\\\'..val + end + val = '\\setstretch{0.8}'..val + tex.print('\\'..rl..'ohead{'..val..'}') +end + +function YAMLvars.prc.rhead(var, val) + YAMLvars.prc.setheader(val, 'r') +end + +function YAMLvars.prc.lhead(var, val) + YAMLvars.prc.setheader(val, 'l') +end + +function YAMLvars.prc.title(var, val) + token.set_macro('@title', val, 'global') +end + +function YAMLvars.prc.author(var, val) + token.set_macro('@author', val, 'global') +end + +function YAMLvars.prc.date(var, val) + token.set_macro('@date', val, 'global') +end + + +-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- + + + +local function prvcmd(cs, val) -- provide command via lua + if token.is_defined(cs) then + tex.print('\\PackageError{YAMLvars}{Variable '..cs..' already defined, could not declare}{}') + else + token.set_macro(cs, val, 'global') + end +end + +local function deccmd(cs, def) + if def == nil then + prvcmd(cs, '\\PackageError{YAMLvars}{Variable "'..cs..'" was declared and used but, not set}{}') + else + prvcmd(cs, def) + end +end + +local function getYAMLfile(y) + local f = io.open(y,"r") + if f ~= nil then + y = f:read('*a') + io.close(f) + return y + else + tex.print('\\PackageError{YAMLvars}{YAML file "'..y..'" not found}{}') + return 'YAMLvars: FileNotFound' + end +end + + +function YAMLvars.declareYAMLvarsStr(y) + local t = YAMLvars.yaml.parse(y) + for var, specs in pairs(t) do + YAMLvars.varspecs[var] = {xfm=YAMLvars.xfrmDefault,prc=YAMLvars.prcDefault,dft=YAMLvars.dftDefault} + if type(specs) == 'string' then + specs = {xfm={specs}} + end + if specs['xfm'] == nil then specs['xfm'] = {} end + for s, p in pairs(specs) do + if s == 'xfm' and type(p) ~= 'table' then p = {p} end + YAMLvars.varspecs[var][s] = p -- set property of var + end + if YAMLvars.varspecs[var]['prc'] == 'gdef' then + deccmd(var, YAMLvars.varspecs[var]['dft']) + elseif YAMLvars.varspecs[var]['prc'] == 'yvdef' then + deccmd('yv--'..var, YAMLvars.varspecs[var]['dft']) + end + end +end + +function YAMLvars.declareYAMLvarsFile(y) + YAMLvars.declareYAMLvarsStr(getYAMLfile(y)) +end + +function YAMLvars.parseYAMLvarsStr(y) + YAMLvars.varsvals = YAMLvars.yaml.parse(y) + for var, val in pairs(YAMLvars.varsvals) do + if YAMLvars.varspecs[var] == nil then + if YAMLvars.allowUndeclared then + if YAMLvars.prcDefault == 'yvdef' then + YAMLvars.prc.yvdef(var, val) + else + prvcmd(var, val) + end + else + tex.print('\\PackageError{YAMLvars}{Variable "'..var..'" set but not declared}{}') + end + else + --if string.sub(val, 1, 2) == '~' then -- todo + -- val = t[string.sub(val, 2, #val)] -- use other val + --end + for _, func in pairs(YAMLvars.varspecs[var]['xfm']) do --apply cleaning functions + local f = YAMLvars.xfm[func] + if f == nil then + tex.print('\\PackageWarning{YAMLvars}{xfm function "'..func..'" not defined, skipping}{}') + else + val = f(var, val) + end + end + f = YAMLvars.prc[YAMLvars.varspecs[var]['prc']] + if f == nil then + tex.print('\\PackageError{YAMLvars}{prc function "'..YAMLvars.varspecs[var]['prc']..'" not defined}{}') + end + f(var, val) -- prc the value of the variable + end + end +end + +function YAMLvars.parseYAMLvarsFile(y) + YAMLvars.parseYAMLvarsStr(getYAMLfile(y)) +end + + + +function YAMLvars.print_varspecs() + local pretty = require('pl.pretty') + texio.write_nl('VVVVVV Var specifications:') + texio.write_nl(pretty.write(YAMLvars.varspecs)) +end + + + +-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- + +-- https://tex.stackexchange.com/questions/38150/in-lualatex-how-do-i-pass-the-content-of-an-environment-to-lua-verbatim +recordedbuf = "" +function readbuf(buf) + i,j = string.find(buf, '\\end{%w+}') + if i==nil then -- if not ending an environment + recordedbuf = recordedbuf .. buf .. "\n" + return "" + else + return nil + end +end + +function startrecording() + recordedbuf = "" + luatexbase.add_to_callback('process_input_buffer', readbuf, 'readbuf') +end + +function stoprecording() + luatexbase.remove_from_callback('process_input_buffer', 'readbuf') + recordedbuf = recordedbuf:gsub("\\end{%w+}\n","") +end + + + +function YAMLvars.doYAMLfiles(t) + if #t == 2 then + YAMLvars.declareYAMLvarsFile(t[1]) + YAMLvars.parseYAMLvarsFile(t[2]) + elseif #t == 1 then + YAMLvars.parseYAMLvarsFile(t[1]) + else + tex.print('\\PackageWarning{YAMLvars}{No .yaml files found in CLI args"}{}') + end +end + +function YAMLvars.getYAMLcli() + local t = {} + if arg then + for i,v in pairs(arg) do + if v:find('.*%.yaml$') then + t[#t+1] = v + end + end + end + help_wrt(t) + return t + end + + +return YAMLvars + + + + --clean = clean or true + --if clean then -- clean first part of yaml string + -- y = clean_tex_spaces(y) + --end +--local function clean_tex_spaces(s) +-- help_wrt(s) +-- if s:sub(1,2) == '%s' then +-- s, _ = s:gsub('%s+','',1) +-- end +-- s, _ = s:gsub('\\par ','\n\n') +-- return s +--end \ No newline at end of file diff --git a/Master/texmf-dist/tex/lualatex/yamlvars/YAMLvars.sty b/Master/texmf-dist/tex/lualatex/yamlvars/YAMLvars.sty new file mode 100644 index 00000000000..fdc39fa3b05 --- /dev/null +++ b/Master/texmf-dist/tex/lualatex/yamlvars/YAMLvars.sty @@ -0,0 +1,59 @@ +% Kale Ewasiuk (kalekje@gmail.com) +% 2021-09-24 +% +% Copyright (C) 2021 Kale Ewasiuk +% +% Permission is hereby granted, free of charge, to any person obtaining a copy +% of this software and associated documentation files (the "Software"), to deal +% in the Software without restriction, including without limitation the rights +% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +% copies of the Software, and to permit persons to whom the Software is +% furnished to do so, subject to the following conditions: +% +% The above copyright notice and this permission notice shall be included in +% all copies or substantial portions of the Software. +% +% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +% ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +% TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +% PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +% SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR +% ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +% ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE +% OR OTHER DEALINGS IN THE SOFTWARE. + + +\NeedsTeXFormat{LaTeX2e} +\ProvidesPackage{YAMLvars}[2021] + +\RequirePackage{luacode} + +\luadirect{YAMLvars = require('YAMLvars')} + +\DeclareOption{useyv}{ + \gdef\yv#1{\csname yv--#1\endcsname} + \luadirect{YAMLvars.prcDefault = 'yvdef'} +} +\DeclareOption{parseCLI}{ + \AtBeginDocument{\luadirect{YAMLvars.doYAMLfiles(YAMLvars.getYAMLcli())}} +} +\DeclareOption{allowundeclared}{ + \luadirect{YAMLvars.allowUndeclared = true} +} + +\DeclareOption*{\PackageWarning{YAMLvars}{Unknown option: '\CurrentOption'}{}} +\ProcessOptions\relax + + +\newcommand{\declareYAMLvarsFile}[1]{\luadirect{YAMLvars.declareYAMLvarsFile(\luastring{#1})}} +\newcommand{\parseYAMLvarsFile}[1]{\luadirect{YAMLvars.parseYAMLvarsFile(\luastring{#1})}} + + +\newenvironment{declareYAMLvars}{\luadirect{startrecording()}}{\luadirect{stoprecording()} +\luadirect{YAMLvars.declareYAMLvarsStr(recordedbuf)} +} + +\newenvironment{parseYAMLvars}{\luadirect{startrecording()}}{\luadirect{stoprecording()} +\luadirect{YAMLvars.parseYAMLvarsStr(recordedbuf)} +} \ No newline at end of file diff --git a/Master/texmf-dist/tex/lualatex/yamlvars/tinyyaml.lua b/Master/texmf-dist/tex/lualatex/yamlvars/tinyyaml.lua new file mode 100644 index 00000000000..b1a64708635 --- /dev/null +++ b/Master/texmf-dist/tex/lualatex/yamlvars/tinyyaml.lua @@ -0,0 +1,816 @@ +------------------------------------------------------------------------------- +-- tinyyaml - YAML subset parser +------------------------------------------------------------------------------- + +local table = table +local string = string +local schar = string.char +local ssub, gsub = string.sub, string.gsub +local sfind, smatch = string.find, string.match +local tinsert, tconcat, tremove = table.insert, table.concat, table.remove +local setmetatable = setmetatable +local pairs = pairs +local type = type +local tonumber = tonumber +local math = math +local getmetatable = getmetatable +local error = error + +local UNESCAPES = { + ['0'] = "\x00", z = "\x00", N = "\x85", + a = "\x07", b = "\x08", t = "\x09", + n = "\x0a", v = "\x0b", f = "\x0c", + r = "\x0d", e = "\x1b", ['\\'] = '\\', +}; + +------------------------------------------------------------------------------- +-- utils +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 + +local function startswith(haystack, needle) + return ssub(haystack, 1, #needle) == needle +end + +local function ltrim(str) + return smatch(str, "^%s*(.-)$") +end + +local function rtrim(str) + return smatch(str, "^(.-)%s*$") +end + +local function trim(str) + return smatch(str, "^%s*(.-)%s*$") +end + +------------------------------------------------------------------------------- +-- Implementation. +-- +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, typ, 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 = typ + cls.__meta = mt + return cls +end + + +local types = { + null = class:def('null'), + map = class:def('map'), + omap = class:def('omap'), + pairs = class:def('pairs'), + set = class:def('set'), + seq = class:def('seq'), + timestamp = class:def('timestamp'), +} + +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() + +function types.timestamp:__init(y, m, d, h, i, s, f, z) + self.year = tonumber(y) + self.month = tonumber(m) + self.day = tonumber(d) + self.hour = tonumber(h or 0) + self.minute = tonumber(i or 0) + self.second = tonumber(s or 0) + if type(f) == 'string' and sfind(f, '^%d+$') then + self.fraction = tonumber(f) * math.pow(10, 3 - #f) + elseif f then + self.fraction = f + else + self.fraction = 0 + end + self.timezone = z +end + +function types.timestamp:__tostring() + return string.format( + '%04d-%02d-%02dT%02d:%02d:%02d.%03d%s', + self.year, self.month, self.day, + self.hour, self.minute, self.second, self.fraction, + self:gettz()) +end + +function types.timestamp:gettz() + if not self.timezone then + return '' + end + if self.timezone == 0 then + return 'Z' + end + local sign = self.timezone > 0 + local z = sign and self.timezone or -self.timezone + local zh = math.floor(z) + local zi = (z - zh) * 60 + return string.format( + '%s%02d:%02d', sign and '+' or '-', zh, zi) +end + + +local function countindent(line) + local _, j = sfind(line, '^%s+') + if not j then + return 0, line + end + return j, ssub(line, j+1) +end + +local function parsestring(line, stopper) + stopper = stopper or '' + local q = ssub(line, 1, 1) + if q == ' ' or q == '\t' then + return parsestring(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 -- flow style + return nil, line + end + if q == '|' or q == '>' then -- block + return nil, line + end + if q == '-' or q == ':' then + if ssub(line, 2, 2) == ' ' or ssub(line, 2, 2) == '\n' 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 ssub(line, 2, 2) == '\n' 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 isemptyline(line) + return line == '' or sfind(line, '^%s*$') or sfind(line, '^%s*#') +end + +local function equalsline(line, needle) + return startswith(line, needle) and isemptyline(ssub(line, #needle+1)) +end + +local function compactifyemptylines(lines) + -- Appends empty lines as "\n" to the end of the nearest preceding non-empty line + local compactified = {} + local lastline = {} + for i = 1, #lines do + local line = lines[i] + if isemptyline(line) then + if #compactified > 0 and i < #lines then + tinsert(lastline, "\n") + end + else + if #lastline > 0 then + tinsert(compactified, tconcat(lastline, "")) + end + lastline = {line} + end + end + if #lastline > 0 then + tinsert(compactified, tconcat(lastline, "")) + end + return compactified +end + +local function checkdupekey(map, key) + 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 + return key +end + +local function parseflowstyle(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) + key.v = checkdupekey(stack[#stack].v, key.v) + 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 = parsestring(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 parseblockstylestring(line, lines, indent) + if #lines == 0 then + error("failed to find multi-line scalar content") + end + local s = {} + local firstindent = -1 + local endline = -1 + for i = 1, #lines do + local ln = lines[i] + local idt = countindent(ln) + if idt <= indent then + break + end + if ln == '' then + tinsert(s, '') + else + if firstindent == -1 then + firstindent = idt + elseif idt < firstindent then + break + end + tinsert(s, ssub(ln, firstindent + 1)) + end + endline = i + end + + local striptrailing = true + local sep = '\n' + local newlineatend = true + if line == '|' then + striptrailing = true + sep = '\n' + newlineatend = true + elseif line == '|+' then + striptrailing = false + sep = '\n' + newlineatend = true + elseif line == '|-' then + striptrailing = true + sep = '\n' + newlineatend = false + elseif line == '>' then + striptrailing = true + sep = ' ' + newlineatend = true + elseif line == '>+' then + striptrailing = false + sep = ' ' + newlineatend = true + elseif line == '>-' then + striptrailing = true + sep = ' ' + newlineatend = false + else + error('invalid blockstyle string:'..line) + end + local _, eonl = s[#s]:gsub('\n', '\n') + s[#s] = rtrim(s[#s]) + if striptrailing then + eonl = 0 + end + if newlineatend then + eonl = eonl + 1 + end + for i = endline, 1, -1 do + tremove(lines, i) + end + return tconcat(s, sep)..string.rep('\n', eonl) +end + +local function parsetimestamp(line) + local _, p1, y, m, d = sfind(line, '^(%d%d%d%d)%-(%d%d)%-(%d%d)') + if not p1 then + return nil, line + end + if p1 == #line then + return types.timestamp(y, m, d), '' + end + local _, p2, h, i, s = sfind(line, '^[Tt ](%d+):(%d+):(%d+)', p1+1) + if not p2 then + return types.timestamp(y, m, d), ssub(line, p1+1) + end + if p2 == #line then + return types.timestamp(y, m, d, h, i, s), '' + end + local _, p3, f = sfind(line, '^%.(%d+)', p2+1) + if not p3 then + p3 = p2 + f = 0 + end + local zc = ssub(line, p3+1, p3+1) + local _, p4, zs, z = sfind(line, '^ ?([%+%-])(%d+)', p3+1) + if p4 then + z = tonumber(z) + local _, p5, zi = sfind(line, '^:(%d+)', p4+1) + if p5 then + z = z + tonumber(zi) / 60 + end + z = zs == '-' and -tonumber(z) or tonumber(z) + elseif zc == 'Z' then + p4 = p3 + 1 + z = 0 + else + p4 = p3 + z = false + end + return types.timestamp(y, m, d, h, i, s, f, z), ssub(line, p4+1) +end + +local function parsescalar(line, lines, indent) + line = trim(line) + line = gsub(line, '^%s*#.*$', '') -- comment only -> '' + line = gsub(line, '^%s*', '') -- trim head spaces + + if line == '' or line == '~' then + return null + end + + local ts, _ = parsetimestamp(line) + if ts then + return ts + end + + local s, _ = parsestring(line) + -- startswith quote ... string + -- not startswith quote ... maybe string + if s and (startswith(line, '"') or startswith(line, "'")) then + return s + end + + if startswith('!', line) then -- unexpected tagchar + error('unsupported line: '..line) + end + + if equalsline(line, '{}') then + return {} + end + if equalsline(line, '[]') then + return {} + end + + if startswith(line, '{') or startswith(line, '[') then + return parseflowstyle(line, lines) + end + + if startswith(line, '|') or startswith(line, '>') then + return parseblockstylestring(line, lines, indent) + end + + -- Regular unquoted string + line = gsub(line, '%s*#.*$', '') -- trim tail comment + 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) -- : int + elseif sfind(v, '^[%+%-]?[0-9]+%.[0-9]+$') then + return tonumber(v) + end + return s or v +end + +local parsemap; -- : func + +local function parseseq(line, lines, indent) + local seq = setmetatable({}, types.seq) + if line ~= '' then + error() + end + while #lines > 0 do + -- Check for a new document + line = lines[1] + if startswith(line, '---') then + while #lines > 0 and not startswith(lines, '---') do + tremove(lines, 1) + end + return seq + end + + -- Check the indent level + local level = countindent(line) + if level < indent then + return seq + elseif level > indent 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]*:%s*$') or sfind(rest, '^[^\'\"%s]*:%s+.') then + -- Inline nested hash + -- There are two patterns need to match as inline nested hash + -- first one should have no other characters except whitespace after `:` + -- and the second one should have characters besides whitespace after `:` + -- + -- value: + -- - foo: + -- bar: 1 + -- + -- and + -- + -- value: + -- - foo: bar + -- + -- And there is one pattern should not be matched, where there is no space after `:` + -- in below, `foo:bar` should be parsed into a single string + -- + -- value: + -- - foo:bar + local indent2 = j + lines[1] = string.rep(' ', indent2)..rest + tinsert(seq, parsemap('', lines, indent2)) + elseif sfind(rest, '^%-%s+') then + -- Inline nested seq + local indent2 = j + lines[1] = string.rep(' ', indent2)..rest + tinsert(seq, parseseq('', 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 = countindent(nextline) + if indent2 == indent then + -- Null seqay entry + tinsert(seq, null) + else + tinsert(seq, parseseq('', lines, indent2)) + end + else + -- - # comment + -- key: value + local nextline = lines[1] + local indent2 = countindent(nextline) + tinsert(seq, parsemap('', lines, indent2)) + end + elseif rest then + -- Array entry with a value + tremove(lines, 1) + tinsert(seq, parsescalar(rest, lines)) + end + end + return seq +end + +local function parseset(line, lines, indent) + if not isemptyline(line) then + error('not seq line: '..line) + end + local set = setmetatable({}, types.set) + while #lines > 0 do + -- Check for a new document + line = lines[1] + if startswith(line, '---') then + while #lines > 0 and not startswith(lines, '---') do + tremove(lines, 1) + end + return set + end + + -- Check the indent level + local level = countindent(line) + if level < indent then + return set + elseif level > indent 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 set + end + end + local rest = ssub(line, j+1) + + if sfind(rest, '^[^\'\"%s]*:') then + -- Inline nested hash + local indent2 = j + lines[1] = string.rep(' ', indent2)..rest + set[parsemap('', lines, indent2)] = true + elseif sfind(rest, '^%s+$') then + tremove(lines, 1) + if #lines == 0 then + tinsert(set, null) + return set + end + if sfind(lines[1], '^%s*%?') then + local indent2 = countindent(lines[1]) + if indent2 == indent then + -- Null array entry + set[null] = true + else + set[parseseq('', lines, indent2)] = true + end + end + + elseif rest then + tremove(lines, 1) + set[parsescalar(rest, lines)] = true + else + error("failed to classify line: "..line) + end + end + return set +end + +function parsemap(line, lines, indent) + if not isemptyline(line) then + error('not map line: '..line) + end + local map = setmetatable({}, types.map) + while #lines > 0 do + -- Check for a new document + line = lines[1] + if startswith(line, '---') then + while #lines > 0 and not startswith(lines, '---') do + tremove(lines, 1) + end + return map + end + + -- Check the indent level + local level, _ = countindent(line) + if level < indent then + return map + elseif level > indent then + error("found bad indenting in line: ".. line) + end + + -- Find the key + local key + local s, rest = parsestring(line) + + -- Quoted keys + if s and startswith(rest, ':') then + local sc = parsescalar(s, {}, 0) + 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 + + key = checkdupekey(map, key) + line = ltrim(line) + + if ssub(line, 1, 1) == '!' then + -- ignore type + local rh = ltrim(ssub(line, 3)) + local typename = smatch(rh, '^!?[^%s]+') + line = ltrim(ssub(rh, #typename+1)) + end + + if not isemptyline(line) then + tremove(lines, 1) + line = ltrim(line) + map[key] = parsescalar(line, lines, indent) + else + -- An indent + tremove(lines, 1) + if #lines == 0 then + map[key] = null + return map; + end + if sfind(lines[1], '^%s*%-') then + local indent2 = countindent(lines[1]) + map[key] = parseseq('', lines, indent2) + elseif sfind(lines[1], '^%s*%?') then + local indent2 = countindent(lines[1]) + map[key] = parseset('', lines, indent2) + else + local indent2 = countindent(lines[1]) + if indent >= indent2 then + -- Null hash entry + map[key] = null + else + map[key] = parsemap('', lines, indent2) + end + end + end + end + return map +end + + +-- : (list)->dict +local function parsedocuments(lines) + lines = compactifyemptylines(lines) + + if sfind(lines[1], '^%%YAML') then tremove(lines, 1) end + + local root = {} + local in_document = false + while #lines > 0 do + local line = lines[1] + -- Do we have a document header? + local docright; + if sfind(line, '^%-%-%-') then + -- Handle scalar documents + docright = ssub(line, 4) + tremove(lines, 1) + in_document = true + end + if docright then + if (not sfind(docright, '^%s+$') and + not sfind(docright, '^%s+#')) then + tinsert(root, parsescalar(docright, lines)) + end + elseif #lines == 0 or startswith(line, '---') then + -- A naked document + tinsert(root, null) + while #lines > 0 and not sfind(lines[1], '---') do + tremove(lines, 1) + end + in_document = false + -- XXX The final '-+$' is to look for -- which ends up being an + -- error later. + elseif not in_document and #root > 0 then + -- only the first document can be explicit + error('parse error: '..line) + elseif sfind(line, '^%s*%-') then + -- An array at the root + tinsert(root, parseseq('', lines, 0)) + elseif sfind(line, '^%s*[^%s]') then + -- A hash at the root + local level = countindent(line) + tinsert(root, parsemap('', lines, level)) + else + -- Shouldn't get here. @lines have whitespace-only lines + -- stripped, and previous match is a line with any + -- non-whitespace. So this clause should only be reachable via + -- a perlbug where \s is not symmetric with \S + + -- uncoverable statement + error('parse error: '..line) + end + end + if #root > 1 and Null.isnull(root[1]) then + tremove(root, 1) + return root + end + return root +end + +--- Parse yaml string into table. +local function parse(source) + local lines = {} + for line in string.gmatch(source .. '\n', '(.-)\r?\n') do + tinsert(lines, line) + end + + local docs = parsedocuments(lines) + if #docs == 1 then + return docs[1] + end + + return docs +end + +return { + version = 0.1, + parse = parse, +} -- cgit v1.2.3