summaryrefslogtreecommitdiff
path: root/macros/luatex
diff options
context:
space:
mode:
authorNorbert Preining <norbert@preining.info>2021-09-25 03:01:31 +0000
committerNorbert Preining <norbert@preining.info>2021-09-25 03:01:31 +0000
commit8322a8a248262d9acaadab2fa12d90012fcf1877 (patch)
tree5dd59d39bf68ca7c1882c1a07aec8470b36dc20e /macros/luatex
parent5d714d9476cbb90b125ff14a9d764ad90bb7c2ed (diff)
CTAN sync 202109250301
Diffstat (limited to 'macros/luatex')
-rw-r--r--macros/luatex/latex/yamlvars/README.md32
-rw-r--r--macros/luatex/latex/yamlvars/YAMLvars.lua369
-rw-r--r--macros/luatex/latex/yamlvars/YAMLvars.pdfbin0 -> 39208 bytes
-rw-r--r--macros/luatex/latex/yamlvars/YAMLvars.sty59
-rw-r--r--macros/luatex/latex/yamlvars/YAMLvars.tex264
-rw-r--r--macros/luatex/latex/yamlvars/tinyyaml.lua816
6 files changed, 1540 insertions, 0 deletions
diff --git a/macros/luatex/latex/yamlvars/README.md b/macros/luatex/latex/yamlvars/README.md
new file mode 100644
index 0000000000..f901e1ef35
--- /dev/null
+++ b/macros/luatex/latex/yamlvars/README.md
@@ -0,0 +1,32 @@
+# YAMLvars -- parse a YAML document and create LaTeX definitons
+
+This LuaLaTeX package provides a YAML parser and some functions to declare and define LaTeX definitions using YAML files.
+
+
+
+# License
+
+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.
+
+
+
diff --git a/macros/luatex/latex/yamlvars/YAMLvars.lua b/macros/luatex/latex/yamlvars/YAMLvars.lua
new file mode 100644
index 0000000000..11f75edda3
--- /dev/null
+++ b/macros/luatex/latex/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/macros/luatex/latex/yamlvars/YAMLvars.pdf b/macros/luatex/latex/yamlvars/YAMLvars.pdf
new file mode 100644
index 0000000000..1d3b1ed2b7
--- /dev/null
+++ b/macros/luatex/latex/yamlvars/YAMLvars.pdf
Binary files differ
diff --git a/macros/luatex/latex/yamlvars/YAMLvars.sty b/macros/luatex/latex/yamlvars/YAMLvars.sty
new file mode 100644
index 0000000000..fdc39fa3b0
--- /dev/null
+++ b/macros/luatex/latex/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/macros/luatex/latex/yamlvars/YAMLvars.tex b/macros/luatex/latex/yamlvars/YAMLvars.tex
new file mode 100644
index 0000000000..e97b325df3
--- /dev/null
+++ b/macros/luatex/latex/yamlvars/YAMLvars.tex
@@ -0,0 +1,264 @@
+% 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.
+
+
+\documentclass{article}
+\usepackage{url}
+\usepackage{YAMLvars}
+\setlength{\parindent}{0ex}
+\setlength{\parskip}{0.75em}
+
+\begin{document}
+
+{\noindent\huge\bfseries YAMLvars \LARGE -- a YAML variable parser for LuaLaTeX}\\
+ 2021-09-24, Kale Ewasiuk, \url{kalekje@gmail.com}
+
+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,
+or just find it easier to read document metadata from a YAML file compared to the
+standard title, author, etc. commands.
+
+\section{Package Options}
+\leavevmode\llap{\texttt{useyv}\ \ \ }By default, when you specify a YAML variable, it will be defined using \texttt{gdef}
+(only if it wasn't defined previously).
+If you use this setting, unless otherwise specified, YAML variables will be accessible under
+the \texttt{\textbackslash yv\{<var>\}} command.
+This also allows numbers and symbols in the variable names.
+Note that internally, the variables are stored in the command sequence
+\texttt{yv--<var>}.
+
+\leavevmode\llap{\texttt{parseCLI}\ \ \ }If this option is enabled, any arguments passed to your lualatex compile
+command that end in ``.yaml'' will be used, separated by a space.
+If two yaml files are passed, the first one will be the declaration file,
+and the second will be the parsing file.
+They will be used at the beginning of the document.
+If one yaml file is passed, it will be treated as a parsing file, so you should
+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}\ \ \ }%
+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.
+
+\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:
+\begin{verbatim}
+\directlua{yaml = YAMLvars.yaml} or
+\directlua{yaml = require('tinyyaml')}
+\end{verbatim}
+The distribution: \url{https://github.com/peposso/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.
+
+\section{Declaring variables}
+A declaration file can either be parsed with the command \texttt{declareYAMLvarsFile} command,
+or, if you want to do it \LaTeX, you can put the YAML code in the \texttt{declareYAMLvars} environment.
+It is a declaring YAML docuemnt is (like all YAML) key-value ditionary:
+The top level key is the name of the variable to be defined/used.
+If the value of the top level is a string: it's interpreted as a single transform function to be applied.
+Otherwise, it must
+be a table that contains at least one of the following keys:\\
+\texttt{xfm} (transform, may be a string or list of strings),\\
+\texttt{prc} (processing, must be a single string), or \\
+\texttt{dft} (default value, if being defined. Must be a string).
+
+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.
+If the token is available, it is set to a package error, so that if the variable no defined later on, an error will
+tell the user they forgot to set it.
+This will be overwritten when you parse the variables and assign a value to it.
+
+You can change the default \texttt{xfm}, \texttt{prc}, or \texttt{dft} by changing
+the value (in Lua): \texttt{YAMLvars.xfmDefault = ''} etc.
+
+Here is an example of a declaration document.
+\begin{verbatim}
+\begin{declareYAMLvars}
+Location: addxspace # sets xfm=addxspace
+People: [arrsortlastnameAZ, list2nl] # BAD! don't do.
+People:
+ xfm: [arrsortlastnameAZ, list2nl] # Correct way
+Company:
+ dft: Amazon # Change default only
+Revisions:
+ dft: '1 & \today & initial version \\'
+ xfm: [sortZA, list2tab]
+Rhead:
+ prc: setRightHead
+\end{declareYAMLvars}
+\end{verbatim}
+
+
+
+\section{Parsing variables}
+A YAML file to be parsed will contain the variables as the top level keys, similar to declaring.
+The value can be anything you want; as long as you have applied appropriate transform and declaring
+functions to it so that it can be useful. For example, a value specified as a YAML list will first be
+interpreted as a Lua table (with numeric indexes/keys). You could declare a series of transforms functions
+to sort this table, map functions, and convert it to a series of \LaTeX \texttt{\textbackslash item}s.
+
+Here is an example of a parsing document.
+
+
+\begin{verbatim}
+\begin{declareYAMLvars}
+Location: Planet Earth
+People: # a YAML list
+ - Some One # turns into Lua table
+ - No Body
+# company assumed Amazon if not set here
+Rhead: \today
+\end{declareYAMLvars}
+\end{verbatim}
+
+
+ \section{xfm -- Transform Functions}
+These functions accept two arguments: \texttt{(var, val)} where \texttt{var} is the variable (or key) and val is the value.
+The transforms are specified as a list and are iteratively applied to the val.
+Usually, the final \texttt{xfm} function should produce a string so it can be defined.
+
+Hint: if for some reason, your \texttt{xfm} and \texttt{prc} depends on other variables,
+you can access them within the function
+with \texttt{YAMLvars.varsvals}
+
+\subsection{Defining your own transform functions}
+After the package is loaded, you may add your function (somewhere in Lua)
+by adding it to the \texttt{YAMLvars.xfm} table.
+For example, if you wanted to wrap a variable's value with ``xxx'', here's how you could do that.
+\begin{verbatim}
+function myfunction(var, val)
+ return 'xxx'..val..'xxx'
+end
+YAMLvars.xfm['addmyfunction'] = myfunction
+\end{verbatim}
+
+ \section{prc -- Processing Functions}
+Like the transform functions, the processing function must accept \texttt{(var, val)}.
+Only one processing function is applied to the final (var, val) after the transforms are done.
+
+This package includes \texttt{gdef} to set a definition, \texttt{yvdef} to define a variable under the \texttt{yv} command.
+\texttt{title, author, date} to set \texttt{\textbackslash @title, \textbackslash @author, \textbackslash @date},
+respectively
+
+ \section{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.
+
+ \pagebreak
+ \vspace*{-5em}
+ \subsection{The main tex template}
+ \begin{verbatim}
+ %% main.tex
+\documentclass{article}
+\usepackage[paperheight=4in,paperwidth=3in,margin=0.25in]{geometry}
+\usepackage[pl,func,extras]{penlight}
+\usepackage[useyv,parseCLI]{YAMLvars} % using command line option to make files
+\usepackage{hyperref}
+\usepackage{xspace}
+\usepackage{luacode}
+
+\setlength{\parindent}{0ex}
+\setlength{\parskip}{0.75em}
+
+\begin{luacode*} -- adding a custom function, put hfill between k-v pairs
+ function YAMLvars.xfm.kv2hfill(var, val)
+ local t = {}
+ for k, v in pairs(val) do
+ t[#t+1] = k..'\\hfill '..tostring(v)
+ end
+ return t
+ end
+\end{luacode*}
+
+
+%! language = yaml
+\begin{declareYAMLvars}
+Customer: addxspace
+Date: addxspace
+Items:
+ xfm: [kv2hfill, arr2itemize]
+\end{declareYAMLvars}
+
+\begin{document}
+ Bill of sale for: \hfill \yv{Customer}\\
+ Purchased: \hfill \yv{Date}\\
+ \begin{itemize}
+ \item[] ITEM \hfill PRICE
+ \yv{Items} % the yaml variable
+ \begin{luacode*}
+ totalcost = pl.tablex.reduce('+',
+ pl.tablex.values(YAMLvars.varsvals['Items']), 0)
+ tex.print('\\item[] TOTAL:\\hfill'..tostring(totalcost))
+ \end{luacode*}
+ \end{itemize}
+\end{document}
+ \end{verbatim}
+
+ \subsection{The lua automation script}
+ \begin{verbatim}
+--automate.lua
+for f in io.popen('dir .'):lines() do -- get all files and info in cwd
+ local i, j = f:find('%S*%.yaml') -- find fnames
+ if i ~= nil then
+ f = f:sub(i,j) -- extract .yaml file name (no space in fname allowed)
+ os.execute('lualatex -output-format=pdf main.tex '.. f)
+ -- compile w/ yaml file as arg
+ local fnew = f:gsub('yaml', 'pdf') -- file name for output pdf
+ os.remove(fnew) -- delete if it exists already
+ os.rename('main.pdf', fnew) -- change main.pdf to same as yaml file name
+ end
+end
+ \end{verbatim}
+
+\subsection{The yaml data files}
+ \begin{verbatim}
+# sale1.yaml
+Customer: Someone Cold
+Date: January 2, 2021
+Items:
+ Toque: 12
+ Mitts: 5.6
+ Boots: 80
+
+# sale2.yaml
+Customer: Someone Warm
+Date: July 1, 2021
+Items:
+ Beer (24 pk): 24
+ Sunscreen: 5
+ Hat: 12
+ \end{verbatim}
+\end{document}
+
diff --git a/macros/luatex/latex/yamlvars/tinyyaml.lua b/macros/luatex/latex/yamlvars/tinyyaml.lua
new file mode 100644
index 0000000000..b1a6470863
--- /dev/null
+++ b/macros/luatex/latex/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<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,
+}