summaryrefslogtreecommitdiff
path: root/Master
diff options
context:
space:
mode:
authorKarl Berry <karl@freefriends.org>2022-01-18 21:31:33 +0000
committerKarl Berry <karl@freefriends.org>2022-01-18 21:31:33 +0000
commit5d65424a72a5ecee6c3ab7e12c21ac49ee887827 (patch)
treeb3f970ed9781dba9bbf11156a7a2c317bcbee536 /Master
parent65f9c8611e6e13d8d69b1c30a793493c32b92dee (diff)
yamlvars (18jan22)
git-svn-id: svn://tug.org/texlive/trunk@61652 c570f23f-e606-0410-a88d-b1316a301751
Diffstat (limited to 'Master')
-rw-r--r--Master/texmf-dist/doc/lualatex/yamlvars/README.md6
-rw-r--r--Master/texmf-dist/doc/lualatex/yamlvars/YAMLvars.pdfbin46890 -> 69229 bytes
-rw-r--r--Master/texmf-dist/doc/lualatex/yamlvars/YAMLvars.tex91
-rw-r--r--Master/texmf-dist/tex/lualatex/yamlvars/YAMLvars.lua57
-rw-r--r--Master/texmf-dist/tex/lualatex/yamlvars/YAMLvars.sty16
-rw-r--r--Master/texmf-dist/tex/lualatex/yamlvars/tinyyaml.lua816
6 files changed, 130 insertions, 856 deletions
diff --git a/Master/texmf-dist/doc/lualatex/yamlvars/README.md b/Master/texmf-dist/doc/lualatex/yamlvars/README.md
index 946ee2e374f..031de6c53a7 100644
--- a/Master/texmf-dist/doc/lualatex/yamlvars/README.md
+++ b/Master/texmf-dist/doc/lualatex/yamlvars/README.md
@@ -1,13 +1,13 @@
-# YAMLvars -- parse a YAML document and create LaTeX definitons
+# YAMLvars -- parse a YAML document and create definitions in LaTeX
This LuaLaTeX package provides a YAML parser and some functions to declare and define LaTeX definitions using YAML files.
-It uses the [`tinyyaml`](https://github.com/api7/lua-tinyyaml) parser.
+It uses the [`markdown-tinyyaml`](https://github.com/api7/lua-tinyyaml) parser that is shipped with the [`markdown`](https://ctan.org/pkg/markdown) package.
# License
-Copyright (C) 2021 Kale Ewasiuk
+Copyright (C) 2021-2022 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
diff --git a/Master/texmf-dist/doc/lualatex/yamlvars/YAMLvars.pdf b/Master/texmf-dist/doc/lualatex/yamlvars/YAMLvars.pdf
index eeeab7ea6c0..974873680d8 100644
--- a/Master/texmf-dist/doc/lualatex/yamlvars/YAMLvars.pdf
+++ b/Master/texmf-dist/doc/lualatex/yamlvars/YAMLvars.pdf
Binary files differ
diff --git a/Master/texmf-dist/doc/lualatex/yamlvars/YAMLvars.tex b/Master/texmf-dist/doc/lualatex/yamlvars/YAMLvars.tex
index 5e32058dabe..057aac5d39d 100644
--- a/Master/texmf-dist/doc/lualatex/yamlvars/YAMLvars.tex
+++ b/Master/texmf-dist/doc/lualatex/yamlvars/YAMLvars.tex
@@ -1,6 +1,6 @@
% Kale Ewasiuk (kalekje@gmail.com)
-% 2021-12-15
-% Copyright (C) 2021 Kale Ewasiuk
+% 2022-01-18
+% Copyright (C) 2021-2022 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
@@ -29,6 +29,7 @@
\newcommand{\cmd}[1]{\texttt{\detokenize{#1}}}
\newcommand{\qcmd}[1]{``\cmd{#1}''}
\usepackage{url}
+\usepackage{xcolor}
\usepackage{showexpl}
\lstset{explpreset={justification=\raggedright,pos=r,wide=true}}
\setlength\ResultBoxRule{0mm}
@@ -53,13 +54,26 @@
-\usepackage{YAMLvars}
+\usepackage[overwritedefs]{YAMLvars}
\title{YAMLvars}
\subtitle{a YAML variable parser for LuaLaTeX}
\begin{document}
+
+
\maketitle
+
+%%%%%
+
+%{NOTE::: \Huge todo use LTXexample and improve examples/testing\\
+%todo need way better error tracing for this pkg}
+
+
+
+%%%%%%%%%%%
+
+
YAMLvars is a LuaLaTeX-based package to help make definitions or produce LaTeX code using a YAML file.
This package might be useful for you if you want to batch create docummnts
by pushing various sets YAML data to a fixed LaTeX template,
@@ -85,25 +99,31 @@ declare the variables somewhere in the preamble.
This option is offered to help with automation scripts.
An example is showin in Section \ref{example}.
-\leavevmode\llap{\texttt{allowundeclared}\ \ \ }%
+\hspace*{3ex}\llcmd{allowundeclared}%
It might be helpful to define something in your YAML parsing doc without declaring it.
If you want this flexibility, use this setting. Note that eisting definitions will not be overwritten and an error
will br thrown if the name exists.
+\llcmd{overwritedefs}Danger! This will allow you to \cmd{gdef} commands with YAML. Caution should be taken to not set definitions like \cmd{begin}, \cmd{section}, etc.
+
\section{Dependencies}
-This package contains the \texttt{tinyyaml} Lua package.
-If you want to use it for other purposes, you can bring it into Lua by either:
+%This package contains the \texttt{tinyyaml} Lua package.
+\llcmd{Note:}This package requires that the \cmd{markdown} (\url{https://ctan.org/pkg/markdown}) be installed. This package does not use the package in its entirety, but rather depends on the YAML interpreter it comes with: \cmd{markdown-tinyyaml.lua}. This dependency is chosen to avoid redundancy in your TeX installation and align development of the \cmd{tinyyaml} Lua package.
+If you want to use the YAML interpreter for other purposes, you can bring it into Lua by either:
\begin{verbatim}
\directlua{yaml = YAMLvars.yaml} or
-\directlua{yaml = require('tinyyaml')}
+\directlua{yaml = require'markdown-tinyyaml'}
\end{verbatim}
-The distribution: \url{https://github.com/peposso/lua-tinyyaml}\\
+The distribution: \url{https://github.com/api7/lua-tinyyaml}\\
The YAML specification: \url{https://yaml.org/spec/}\\
-Many of the ``transform'' and ``processing'' functions built-in to this package rely on other packages.
-\texttt{penlight},
-\texttt{xspace},
-\texttt{hyperref} for example.
+Many of the ``transform'' and ``processing'' functions built-in to this package rely on other packages,
+like
+\texttt{hyperref}, for example, but it is not loaded, and this package will only load
+\cmd{penlight},
+\cmd{luacode},
+\cmd{xspace}, and
+\cmd{etoolbox}.
\section{Declaring variables}
A declaration file can either be parsed with the command \texttt{declareYAMLvarsFile} command,
@@ -117,6 +137,9 @@ be a table that contains at least one of the following keys:\\
\texttt{prc} (processing, must be a single string), or \\
\texttt{dft} (default value, if being defined. Must be a string).
+If you want to change the way a variable is initialized, you can change the function
+\cmd{YAMLvars.dec.PRC = function (var) ... end } where \cmd{PRC} is how the variable will be processed (\cmd{gdef}, \cmd{yvdef}, \cmd{length}, or something of your choosing).
+
The default value for variables is the Lua \texttt{nil}.
YAMLvars will first check if the definition exists, if so, an error will be thrown
so that we avoid overwriting.
@@ -210,7 +233,49 @@ This package includes \texttt{gdef} to set a definition, \texttt{yvdef} to defin
\texttt{title, author, date} to set \texttt{\textbackslash @title, \textbackslash @author, \textbackslash @date},
respectively
- \section{Example}\label{example}
+\pagebreak
+
+\section{Some Examples}
+
+\begin{LTXexample}
+%! language = yaml
+\begin{declareYAMLvars}
+address:
+ xfm:
+ - list2nl
+ - = x..'!!!'
+name: null
+
+title:
+ xfm:
+ - lb2nl
+# - / YAMLvars.prvcmd(titletext, YAMLvars.varsvals['atitle']:gsub('\n', ' ')..'\\xspace{}')
+\end{declareYAMLvars}
+
+%! language = yaml
+\begin{parseYAMLvars}
+title: |-
+ A Multiline
+ Monumental Title!
+
+name: Joe Smith
+address:
+ - 1234 Fake St.
+ - City
+\end{parseYAMLvars}
+
+\title
+
+%\titletext!
+
+\name
+
+\address
+
+\end{LTXexample}
+
+\pagebreak
+ \section{Automation Example}\label{example}
Suppose you had a number of bills of sales in yaml format and wanted to produce some nice pdfs.
The following code shows how this could be done.
diff --git a/Master/texmf-dist/tex/lualatex/yamlvars/YAMLvars.lua b/Master/texmf-dist/tex/lualatex/yamlvars/YAMLvars.lua
index 72e4d6ad1a4..93234e5d596 100644
--- a/Master/texmf-dist/tex/lualatex/yamlvars/YAMLvars.lua
+++ b/Master/texmf-dist/tex/lualatex/yamlvars/YAMLvars.lua
@@ -1,6 +1,6 @@
--% Kale Ewasiuk (kalekje@gmail.com)
---% 2021-12-15
---% Copyright (C) 2021 Kale Ewasiuk
+--% 2022-01-18
+--% Copyright (C) 2021-2022 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
@@ -61,6 +61,7 @@ YAMLvars.dftDefault = nil
YAMLvars.xfmDefault = {}
YAMLvars.allowUndeclared = false
+YAMLvars.overwritedefs = false
YAMLvars.valTemp = ''
YAMLvars.varTemp = ''
@@ -69,7 +70,9 @@ YAMLvars.tabmidrule = 'hline'
YAMLvars.debug = false
-YAMLvars.yaml = require('tinyyaml')
+YAMLvars.yaml = require'markdown-tinyyaml' -- note: YAMLvars.sty will have checked existence of this already
+local pl = _G['penlight'] or _G['pl'] -- penlight for this namespace is pl
+
function YAMLvars.debugtalk(s, ss)
if YAMLvars.debug then
@@ -77,9 +80,10 @@ function YAMLvars.debugtalk(s, ss)
end
end
+
-- xfm functions (transforms) -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
function YAMLvars.xfm.addxspace(var, val)
- return val .. '\\xspace'
+ return val .. '\\xspace{}'
end
function YAMLvars.xfm.tab2arr(var, val)
@@ -99,10 +103,10 @@ function YAMLvars.xfm.arr2tabular(var, val)
end
-function YAMLvars.xfm.arr2itemize(var, val)
+function YAMLvars.xfm.list2items(var, val) -- todo should be list2item
return pl.List(val):map('\\item '.._1):join(' ')
end
-
+YAMLvars.xfm.arr2itemize = YAMLvars.xfm.list2items
function YAMLvars.xfm.arrsortAZ(var, val)
return pl.List(val):sort(pl.operator.strlt)
@@ -126,12 +130,15 @@ function YAMLvars.xfm.arrsortlastnameAZ(var, val)
end
-- todo need distinction beyyween table and penlight list ???
function YAMLvars.xfm.list2nl(var, val)
- val = pl.array2d.map_slice1(_1..'\\\\', val, 1,-2)
- return val:join('')
- --return pl.tablex.reduce(_1.._2, val, '')
+ return pl.tablex.join(val,'\\\\ ')
end
-function YAMLvars.xfm.lb2nl(var, val)
+ --val = pl.array2d.map_slice1(_1..'\\\\', val, 1,-2)
+ --return val:join('')
+ --return pl.tablex.reduce(_1.._2, val, '')
+
+
+function YAMLvars.xfm.lb2nl(var, val) --linebreak in text 2 new line
val, _ = val:gsub('\n','\\\\ ')
return val
end
@@ -265,7 +272,7 @@ end
function YAMLvars.prvcmd(cs, val) -- provide command via lua
- if token.is_defined(cs) then
+ if token.is_defined(cs) and (not YAMLvars.overwritedefs) then
tex.print('\\PackageError{YAMLvars}{Variable '..cs..' already defined, could not declare}{}')
else
token.set_macro(cs, val, 'global')
@@ -339,21 +346,29 @@ local function sub_lua_var(s, v1, v2)
return s:gsub('([%A?%-?])('..v1..')([%W?%-?])', '%1'..v2..'%3') -- replace x variables
end
+local _YV_invalid_expression = '\1 invalid expression'
+local _YV_no_return = '\2 no return val'
local function eval_expr(func, var, val)
- local s, c = func:gsub('^[=/]', {['/'] = '', ['='] = 'YAMLvars.valTemp = '}, 1) -- / is run code, = sets val = code
+ local s, c = func:gsub('^[=/]', {['/'] = '\2', ['='] = 'YAMLvars.valTemp = '}, 1) -- / is run code, = sets val = code
if c == 0 then
- return nil
+ return _YV_invalid_expression
else
--help_wrt(s, var)
--help_wrt(val, var)
YAMLvars.valTemp = val
YAMLvars.varTemp = var
--help_wrt(s, var)
+ s, c = s:gsub('\2', '') -- strip \2 that might have appeared if / was applied
s = sub_lua_var(' '..s, 'x', 'YAMLvars.valTemp')
s = sub_lua_var(s, 'v', 'YAMLvars.varTemp')
--help_wrt(s, var)
- loadstring(s)()
- --help_wrt(val, var)
+ local f, err = pcall(loadstring(s))
+ if not f then
+ tex.print('\\PackageError{YAMLvars}{xfm with "= or /" error on var "'..var..'"}{}') --
+ end
+ if c > 0 then
+ return _YV_no_return
+ end
return YAMLvars.valTemp
end
end
@@ -363,8 +378,9 @@ local function transform_and_prc(var, val)
local f = YAMLvars.xfm[func]
if f == nil then
local val2 = eval_expr(func, var, val)
- if val2 == nil then
- tex.print('\\PackageWarning{YAMLvars}{xfm function "'..func..'" not defined, skipping}{}')
+ if val2 == _YV_invalid_expression then
+ tex.print('\\PackageError{YAMLvars}{xfm function "'..func..'" not defined or invalid expression passed on var "'..var..'"}{}')
+ elseif val == _YV_no_return then
else
val = val2
end
@@ -374,7 +390,7 @@ local function transform_and_prc(var, val)
end
f = YAMLvars.prc[YAMLvars.varspecs[var]['prc']]
if f == nil then
- tex.print('\\PackageError{YAMLvars}{prc function "'..YAMLvars.varspecs[var]['prc']..'" not defined}{}')
+ tex.print('\\PackageError{YAMLvars}{prc function "'..YAMLvars.varspecs[var]['prc']..'" on var "'..var..'" not defined}{}')
end
f(var, val) -- prc the value of the variable
end
@@ -383,7 +399,10 @@ function YAMLvars.parseYAMLvarsStr(y)
YAMLvars.varsvals = YAMLvars.yaml.parse(y)
for var, val in pairs(YAMLvars.varsvals) do
if YAMLvars.varspecs[var] == nil then
- check_def(var, val)
+ check_def(var, val) -- if not declared
+ -- todo consider free form parse declaring
+ -- variable name: {xfm:, dec:, prc:, val: }
+ -- definitely doable here
else
transform_and_prc(var, val)
end
diff --git a/Master/texmf-dist/tex/lualatex/yamlvars/YAMLvars.sty b/Master/texmf-dist/tex/lualatex/yamlvars/YAMLvars.sty
index 23928006519..73c40ba6931 100644
--- a/Master/texmf-dist/tex/lualatex/yamlvars/YAMLvars.sty
+++ b/Master/texmf-dist/tex/lualatex/yamlvars/YAMLvars.sty
@@ -1,6 +1,6 @@
% Kale Ewasiuk (kalekje@gmail.com)
-% 2021-12-15
-% Copyright (C) 2021 Kale Ewasiuk
+% 2022-01-18
+% Copyright (C) 2021-2022 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
@@ -23,15 +23,17 @@
% OR OTHER DEALINGS IN THE SOFTWARE.
+
\NeedsTeXFormat{LaTeX2e}
-\ProvidesPackage{YAMLvars}[2021-12-15]
+\ProvidesPackage{YAMLvars}[2022-01-18]
+
+\IfFileExists{markdown-tinyyaml.lua}{}{\PackageError{YAMLvars}{This package requires installation of the 'markdown' package, please install it and try again}{}}
\RequirePackage{luacode}
\RequirePackage{xspace}
\RequirePackage{etoolbox}
\RequirePackage[pl,extras]{penlight}
-
\luadirect{YAMLvars = require('YAMLvars')}
\DeclareOption{useyv}{
@@ -43,7 +45,11 @@
}
\DeclareOption{allowundeclared}{
\luadirect{YAMLvars.allowUndeclared = true}
-}\DeclareOption{debug}{
+}
+\DeclareOption{overwritedefs}{
+ \luadirect{YAMLvars.overwritedefs = true}
+}
+\DeclareOption{debug}{
\luadirect{YAMLvars.debug = true}
}
diff --git a/Master/texmf-dist/tex/lualatex/yamlvars/tinyyaml.lua b/Master/texmf-dist/tex/lualatex/yamlvars/tinyyaml.lua
deleted file mode 100644
index b1a64708635..00000000000
--- a/Master/texmf-dist/tex/lualatex/yamlvars/tinyyaml.lua
+++ /dev/null
@@ -1,816 +0,0 @@
--------------------------------------------------------------------------------
--- 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<str>)->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,
-}