aboutsummaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/lyaml/explicit.lua120
-rw-r--r--lib/lyaml/functional.lua87
-rw-r--r--lib/lyaml/implicit.lua283
-rw-r--r--lib/lyaml/init.lua534
4 files changed, 1024 insertions, 0 deletions
diff --git a/lib/lyaml/explicit.lua b/lib/lyaml/explicit.lua
new file mode 100644
index 000000000000..98a38331ead6
--- /dev/null
+++ b/lib/lyaml/explicit.lua
@@ -0,0 +1,120 @@
+-- LYAML parse explicit token values.
+-- Written by Gary V. Vaughan, 2015
+--
+-- Copyright(C) 2015-2022 Gary V. Vaughan
+--
+-- 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.
+
+--- @module lyaml.explicit
+
+local functional = require 'lyaml.functional'
+local implicit = require 'lyaml.implicit'
+
+local NULL = functional.NULL
+local anyof = functional.anyof
+local id = functional.id
+
+
+local yn = {y=true, Y=true, n=false, N=false}
+
+
+--- Parse the value following an explicit `!!bool` tag.
+-- @function bool
+-- @param value token
+-- @treturn[1] bool boolean equivalent, if a valid value was recognized
+-- @treturn[2] nil otherwise, nil
+-- @usage maybe_bool = explicit.bool(tagarg)
+local bool = anyof {
+ implicit.bool,
+ function(x) return yn[x] end,
+}
+
+
+--- Return a function that converts integer results to equivalent float.
+-- @tparam function fn token parsing function
+-- @treturn function new function that converts int results to float
+-- @usage maybe_float = maybefloat(implicit.decimal)(tagarg)
+local function maybefloat(fn)
+ return function(...)
+ local r = fn(...)
+ if type(r) == 'number' then
+ return r + 0.0
+ end
+ end
+end
+
+
+--- Parse the value following an explicit `!!float` tag.
+-- @function float
+-- @param value token
+-- @treturn[1] number float equivalent, if a valid value was recognized
+-- @treturn[2] nil otherwise, nil
+-- @usage maybe_float = explicit.float(tagarg)
+local float = anyof {
+ implicit.float,
+ implicit.nan,
+ implicit.inf,
+ maybefloat(implicit.octal),
+ maybefloat(implicit.decimal),
+ maybefloat(implicit.hexadecimal),
+ maybefloat(implicit.binary),
+ implicit.sexfloat,
+}
+
+
+--- Parse the value following an explicit `!!int` tag.
+-- @function int
+-- @param value token
+-- @treturn[1] int integer equivalent, if a valid value was recognized
+-- @treturn[2] nil otherwise, nil
+-- @usage maybe_int = explicit.int(tagarg)
+local int = anyof {
+ implicit.octal,
+ implicit.decimal,
+ implicit.hexadecimal,
+ implicit.binary,
+ implicit.sexagesimal,
+}
+
+
+--- Parse an explicit `!!null` tag.
+-- @treturn lyaml.null
+-- @usage null = explicit.null(tagarg)
+local function null()
+ return NULL
+end
+
+
+--- Parse the value following an explicit `!!str` tag.
+-- @function str
+-- @tparam string value token
+-- @treturn string *value* which was a string already
+-- @usage tagarg = explicit.str(tagarg)
+local str = id
+
+
+--- @export
+return {
+ bool = bool,
+ float = float,
+ int = int,
+ null = null,
+ str = str,
+}
diff --git a/lib/lyaml/functional.lua b/lib/lyaml/functional.lua
new file mode 100644
index 000000000000..556e9489505a
--- /dev/null
+++ b/lib/lyaml/functional.lua
@@ -0,0 +1,87 @@
+-- Minimal functional programming utilities.
+-- Written by Gary V. Vaughan, 2015
+--
+-- Copyright(C) 2015-2022 Gary V. Vaughan
+--
+-- 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.
+
+--- @module lyaml.functional
+
+
+--- `lyaml.null` value.
+-- @table NULL
+local NULL = setmetatable({}, {_type='LYAML null'})
+
+
+--- `lyaml.null` predicate.
+-- @param x operand
+-- @treturn bool `true` if *x* is `lyaml.null`.
+local function isnull(x)
+ return(getmetatable(x) or {})._type == 'LYAML null'
+end
+
+
+--- Callable predicate.
+-- @param x operand
+-- @treturn bool `true` if *x* is a function has a __call metamethod
+-- @usage r = iscallable(x) and x(...)
+local function iscallable(x)
+ if type(x) ~= 'function' then
+ x =(getmetatable(x) or {}).__call
+ end
+ if type(x) == 'function' then
+ return x
+ end
+end
+
+
+--- Compose a function to try each callable with supplied args.
+-- @tparam table fns list of functions to try
+-- @treturn function a new function to call *...* functions, stopping
+-- and returning the first non-nil result, if any
+local function anyof(fns)
+ return function(...)
+ for _, fn in ipairs(fns) do
+ if iscallable(fn) then
+ local r = fn(...)
+ if r ~= nil then
+ return r
+ end
+ end
+ end
+ end
+end
+
+
+--- Return arguments unchanged.
+-- @param ... arguments
+-- @return *...*
+local function id(...)
+ return ...
+end
+
+--- @export
+return {
+ NULL = NULL,
+ anyof = anyof,
+ id = id,
+ iscallable = iscallable,
+ isnull = isnull,
+}
diff --git a/lib/lyaml/implicit.lua b/lib/lyaml/implicit.lua
new file mode 100644
index 000000000000..fe58025b560d
--- /dev/null
+++ b/lib/lyaml/implicit.lua
@@ -0,0 +1,283 @@
+-- LYAML parse implicit type tokens.
+-- Written by Gary V. Vaughan, 2015
+--
+-- Copyright(C) 2015-2022 Gary V. Vaughan
+--
+-- 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.
+
+--- @module lyaml.implicit
+
+
+local NULL = require 'lyaml.functional'.NULL
+local find = string.find
+local floor = math.floor
+local gsub = string.gsub
+local sub = string.sub
+
+local tointeger = (function(f)
+ if not tointeger then
+ -- No host tointeger implementation, use our own.
+ return function(x)
+ if type(x) == 'number' and x - floor(x) == 0.0 then
+ return x
+ end
+ end
+
+ elseif f '1' ~= nil then
+ -- Don't perform implicit string-to-number conversion!
+ return function(x)
+ if type(x) == 'number' then
+ return tointeger(x)
+ end
+ end
+ end
+
+ -- Host tointeger is good!
+ return f
+end)(math.tointeger)
+
+
+local function int(x)
+ local r = tonumber(x)
+ if r ~= nil then
+ return tointeger(r)
+ end
+end
+
+
+local is_null = {['']=true, ['~']=true, null=true, Null=true, NULL=true}
+
+
+--- Parse a null token to a null value.
+-- @param value token
+-- @return[1] lyaml.null, for an empty string or literal ~
+-- @return[2] nil otherwise, nil
+-- @usage maybe_null = implicit.null(token)
+local function null(value)
+ if is_null[value] then
+ return NULL
+ end
+end
+
+
+local to_bool = {
+ ['true'] = true, True = true, TRUE = true,
+ ['false'] = false, False = false, FALSE = false,
+ yes = true, Yes = true, YES = true,
+ no = false, No = false, NO = false,
+ on = true, On = true, ON = true,
+ off = false, Off = false, OFF = false,
+}
+
+
+--- Parse a boolean token to the equivalent value.
+-- Treats capilalized, lower and upper-cased variants of true/false,
+-- yes/no or on/off tokens as boolean `true` and `false` values.
+-- @param value token
+-- @treturn[1] bool if a valid boolean token was recognized
+-- @treturn[2] nil otherwise, nil
+-- @usage maybe_bool = implicit.bool(token)
+local function bool(value)
+ return to_bool[value]
+end
+
+
+--- Parse a binary token, such as '0b1010\_0111\_0100\_1010\_1110'.
+-- @tparam string value token
+-- @treturn[1] int integer equivalent, if a valid token was recognized
+-- @treturn[2] nil otherwise, nil
+-- @usage maybe_int = implicit.binary(value)
+local function binary(value)
+ local r
+ gsub(value, '^([+-]?)0b_*([01][01_]+)$', function(sign, rest)
+ r = 0
+ gsub(rest, '_*(.)', function(digit)
+ r = r * 2 + int(digit)
+ end)
+ if sign == '-' then
+ r = r * -1
+ end
+ end)
+ return r
+end
+
+
+--- Parse an octal token, such as '012345'.
+-- @tparam string value token
+-- @treturn[1] int integer equivalent, if a valid token was recognized
+-- @treturn[2] nil otherwise, nil
+-- @usage maybe_int = implicit.octal(value)
+local function octal(value)
+ local r
+ gsub(value, '^([+-]?)0_*([0-7][0-7_]*)$', function(sign, rest)
+ r = 0
+ gsub(rest, '_*(.)', function(digit)
+ r = r * 8 + int(digit)
+ end)
+ if sign == '-' then
+ r = r * -1
+ end
+ end)
+ return r
+end
+
+
+--- Parse a decimal token, such as '0' or '12345'.
+-- @tparam string value token
+-- @treturn[1] int integer equivalent, if a valid token was recognized
+-- @treturn[2] nil otherwise, nil
+-- @usage maybe_int = implicit.decimal(value)
+local function decimal(value)
+ local r
+ gsub(value, '^([+-]?)_*([0-9][0-9_]*)$', function(sign, rest)
+ rest = gsub(rest, '_', '')
+ if rest == '0' or #rest > 1 or sub(rest, 1, 1) ~= '0' then
+ r = int(rest)
+ if sign == '-' then
+ r = r * -1
+ end
+ end
+ end)
+ return r
+end
+
+
+--- Parse a hexadecimal token, such as '0xdeadbeef'.
+-- @tparam string value token
+-- @treturn[1] int integer equivalent, if a valid token was recognized
+-- @treturn[2] nil otherwise, nil
+-- @usage maybe_int = implicit.hexadecimal(value)
+local function hexadecimal(value)
+ local r
+ gsub(value, '^([+-]?)(0x_*[0-9a-fA-F][0-9a-fA-F_]*)$', function(sign, rest)
+ rest = gsub(rest, '_', '')
+ r = int(rest)
+ if sign == '-' then
+ r = r * -1
+ end
+ end)
+ return r
+end
+
+
+--- Parse a sexagesimal token, such as '190:20:30'.
+-- Useful for times and angles.
+-- @tparam string value token
+-- @treturn[1] int integer equivalent, if a valid token was recognized
+-- @treturn[2] nil otherwise, nil
+-- @usage maybe_int = implicit.sexagesimal(value)
+local function sexagesimal(value)
+ local r
+ gsub(value, '^([+-]?)([0-9]+:[0-5]?[0-9][:0-9]*)$', function(sign, rest)
+ r = 0
+ gsub(rest, '([0-9]+):?', function(digit)
+ r = r * 60 + int(digit)
+ end)
+ if sign == '-' then
+ r = r * -1
+ end
+ end)
+ return r
+end
+
+
+local isnan = {['.nan']=true, ['.NaN']=true, ['.NAN']=true}
+
+
+--- Parse a `nan` token.
+-- @tparam string value token
+-- @treturn[1] nan not-a-number, if a valid token was recognized
+-- @treturn[2] nil otherwise, nil
+-- @usage maybe_nan = implicit.nan(value)
+local function nan(value)
+ if isnan[value] then
+ return 0/0
+ end
+end
+
+
+local isinf = {
+ ['.inf'] = math.huge, ['.Inf'] = math.huge, ['.INF'] = math.huge,
+ ['+.inf'] = math.huge, ['+.Inf'] = math.huge, ['+.INF'] = math.huge,
+ ['-.inf'] = -math.huge, ['-.Inf'] = -math.huge, ['-.INF'] = -math.huge,
+}
+
+
+--- Parse a signed `inf` token.
+-- @tparam string value token
+-- @treturn[1] number plus/minus-infinity, if a valid token was recognized
+-- @treturn[2] nil otherwise, nil
+-- @usage maybe_inf = implicit.inf(value)
+local function inf(value)
+ return isinf[value]
+end
+
+
+--- Parse a floating point number token, such as '1e-3' or '-0.12'.
+-- @tparam string value token
+-- @treturn[1] number float equivalent, if a valid token was recognized
+-- @treturn[2] nil otherwise, nil
+-- @usage maybe_float = implicit.float(value)
+local function float(value)
+ local r = tonumber((gsub(value, '_', '')))
+ if r and find(value, '[%.eE]') then
+ return r
+ end
+end
+
+
+--- Parse a sexagesimal float, such as '190:20:30.15'.
+-- Useful for times and angles.
+-- @tparam string value token
+-- @treturn[1] number float equivalent, if a valid token was recognized
+-- @treturn[2] nil otherwise, nil
+-- @usage maybe_float = implicit.sexfloat(value)
+local function sexfloat(value)
+ local r
+ gsub(value, '^([+-]?)([0-9]+:[0-5]?[0-9][:0-9]*)(%.[0-9]+)$',
+ function(sign, rest, float)
+ r = 0
+ gsub(rest, '([0-9]+):?', function(digit)
+ r = r * 60 + int(digit)
+ end)
+ r = r + tonumber(float)
+ if sign == '-' then
+ r = r * -1
+ end
+ end
+ )
+ return r
+end
+
+
+--- @export
+return {
+ binary = binary,
+ decimal = decimal,
+ float = float,
+ hexadecimal = hexadecimal,
+ inf = inf,
+ nan = nan,
+ null = null,
+ octal = octal,
+ sexagesimal = sexagesimal,
+ sexfloat = sexfloat,
+ bool = bool,
+}
diff --git a/lib/lyaml/init.lua b/lib/lyaml/init.lua
new file mode 100644
index 000000000000..95e4036ea7c9
--- /dev/null
+++ b/lib/lyaml/init.lua
@@ -0,0 +1,534 @@
+-- Transform between YAML 1.1 streams and Lua table representations.
+-- Written by Gary V. Vaughan, 2013
+--
+-- Copyright(C) 2013-2022 Gary V. Vaughan
+--
+-- 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.
+--
+-- Portions of this software were inspired by an earlier LibYAML binding
+-- by Andrew Danforth <acd@weirdness.net>
+
+--- @module lyaml
+
+
+local explicit = require 'lyaml.explicit'
+local functional = require 'lyaml.functional'
+local implicit = require 'lyaml.implicit'
+local yaml = require 'yaml'
+
+local NULL = functional.NULL
+local anyof = functional.anyof
+local find = string.find
+local format = string.format
+local gsub = string.gsub
+local id = functional.id
+local isnull = functional.isnull
+local match = string.match
+
+
+local TAG_PREFIX = 'tag:yaml.org,2002:'
+
+
+local function tag(name)
+ return TAG_PREFIX .. name
+end
+
+
+local default = {
+ -- Tag table to lookup explicit scalar conversions.
+ explicit_scalar = {
+ [tag 'bool'] = explicit.bool,
+ [tag 'float'] = explicit.float,
+ [tag 'int'] = explicit.int,
+ [tag 'null'] = explicit.null,
+ [tag 'str'] = explicit.str,
+ },
+ -- Order is important, so we put most likely and fastest nearer
+ -- the top to reduce average number of comparisons and funcalls.
+ implicit_scalar = anyof {
+ implicit.null,
+ implicit.octal, -- subset of decimal, must come earlier
+ implicit.decimal,
+ implicit.float,
+ implicit.bool,
+ implicit.inf,
+ implicit.nan,
+ implicit.hexadecimal,
+ implicit.binary,
+ implicit.sexagesimal,
+ implicit.sexfloat,
+ id,
+ },
+}
+
+
+-- Metatable for Dumper objects.
+local dumper_mt = {
+ __index = {
+ -- Emit EVENT to the LibYAML emitter.
+ emit = function(self, event)
+ return self.emitter.emit(event)
+ end,
+
+ -- Look up an anchor for a repeated document element.
+ get_anchor = function(self, value)
+ local r = self.anchors[value]
+ if r then
+ self.aliased[value], self.anchors[value] = self.anchors[value], nil
+ end
+ return r
+ end,
+
+ -- Look up an already anchored repeated document element.
+ get_alias = function(self, value)
+ return self.aliased[value]
+ end,
+
+ -- Dump ALIAS into the event stream.
+ dump_alias = function(self, alias)
+ return self:emit {
+ type = 'ALIAS',
+ anchor = alias,
+ }
+ end,
+
+ -- Dump MAP into the event stream.
+ dump_mapping = function(self, map)
+ local alias = self:get_alias(map)
+ if alias then
+ return self:dump_alias(alias)
+ end
+
+ self:emit {
+ type = 'MAPPING_START',
+ anchor = self:get_anchor(map),
+ style = 'BLOCK',
+ }
+ for k, v in pairs(map) do
+ self:dump_node(k)
+ self:dump_node(v)
+ end
+ return self:emit {type='MAPPING_END'}
+ end,
+
+ -- Dump SEQUENCE into the event stream.
+ dump_sequence = function(self, sequence)
+ local alias = self:get_alias(sequence)
+ if alias then
+ return self:dump_alias(alias)
+ end
+
+ self:emit {
+ type = 'SEQUENCE_START',
+ anchor = self:get_anchor(sequence),
+ style = 'BLOCK',
+ }
+ for _, v in ipairs(sequence) do
+ self:dump_node(v)
+ end
+ return self:emit {type='SEQUENCE_END'}
+ end,
+
+ -- Dump a null into the event stream.
+ dump_null = function(self)
+ return self:emit {
+ type = 'SCALAR',
+ value = '~',
+ plain_implicit = true,
+ quoted_implicit = true,
+ style = 'PLAIN',
+ }
+ end,
+
+ -- Dump VALUE into the event stream.
+ dump_scalar = function(self, value)
+ local alias = self:get_alias(value)
+ if alias then
+ return self:dump_alias(alias)
+ end
+
+ local anchor = self:get_anchor(value)
+ local itsa = type(value)
+ local style = 'PLAIN'
+ if itsa == 'string' and self.implicit_scalar(value) ~= value then
+ -- take care to round-trip strings that look like scalars
+ style = 'SINGLE_QUOTED'
+ elseif value == math.huge then
+ value = '.inf'
+ elseif value == -math.huge then
+ value = '-.inf'
+ elseif value ~= value then
+ value = '.nan'
+ elseif itsa == 'number' or itsa == 'boolean' then
+ value = tostring(value)
+ elseif itsa == 'string' and find(value, '\n') then
+ style = 'LITERAL'
+ end
+ return self:emit {
+ type = 'SCALAR',
+ anchor = anchor,
+ value = value,
+ plain_implicit = true,
+ quoted_implicit = true,
+ style = style,
+ }
+ end,
+
+ -- Decompose NODE into a stream of events.
+ dump_node = function(self, node)
+ local itsa = type(node)
+ if isnull(node) then
+ return self:dump_null()
+ elseif itsa == 'string' or itsa == 'boolean' or itsa == 'number' then
+ return self:dump_scalar(node)
+ elseif itsa == 'table' then
+ -- Something is only a sequence if its keys start at 1
+ -- and are consecutive integers without any jumps.
+ local prior_key = 0
+ local is_pure_sequence = true
+ local i, v = next(node, nil)
+ while i and is_pure_sequence do
+ if type(i) ~= "number" or (prior_key + 1 ~= i) then
+ is_pure_sequence = false -- breaks the loop
+ else
+ prior_key = i
+ i, v = next(node, prior_key)
+ end
+ end
+ if is_pure_sequence then
+ -- Only sequentially numbered integer keys starting from 1.
+ return self:dump_sequence(node)
+ else
+ -- Table contains non sequential integer keys or mixed keys.
+ return self:dump_mapping(node)
+ end
+ else -- unsupported Lua type
+ error("cannot dump object of type '" .. itsa .. "'", 2)
+ end
+ end,
+
+ -- Dump DOCUMENT into the event stream.
+ dump_document = function(self, document)
+ self:emit {type='DOCUMENT_START'}
+ self:dump_node(document)
+ return self:emit {type='DOCUMENT_END'}
+ end,
+ },
+}
+
+
+-- Emitter object constructor.
+local function Dumper(opts)
+ local anchors = {}
+ for k, v in pairs(opts.anchors) do
+ anchors[v] = k
+ end
+ local object = {
+ aliased = {},
+ anchors = anchors,
+ emitter = yaml.emitter(),
+ implicit_scalar = opts.implicit_scalar,
+ }
+ return setmetatable(object, dumper_mt)
+end
+
+
+--- Dump options table.
+-- @table dumper_opts
+-- @tfield table anchors map initial anchor names to values
+-- @tfield function implicit_scalar parse implicit scalar values
+
+
+--- Dump a list of Lua tables to an equivalent YAML stream.
+-- @tparam table documents a sequence of Lua tables.
+-- @tparam[opt] dumper_opts opts initialisation options
+-- @treturn string equivalest YAML stream
+local function dump(documents, opts)
+ opts = opts or {}
+
+ -- backwards compatibility
+ if opts.anchors == nil and opts.implicit_scalar == nil then
+ opts = {anchors=opts}
+ end
+
+ local dumper = Dumper {
+ anchors = opts.anchors or {},
+ implicit_scalar = opts.implicit_scalar or default.implicit_scalar,
+ }
+
+ dumper:emit {type='STREAM_START', encoding='UTF8'}
+ for _, document in ipairs(documents) do
+ dumper:dump_document(document)
+ end
+ local ok, stream = dumper:emit {type='STREAM_END'}
+ return stream
+end
+
+
+-- We save anchor types that will match the node type from expanding
+-- an alias for that anchor.
+local alias_type = {
+ MAPPING_END = 'MAPPING_END',
+ MAPPING_START = 'MAPPING_END',
+ SCALAR = 'SCALAR',
+ SEQUENCE_END = 'SEQUENCE_END',
+ SEQUENCE_START = 'SEQUENCE_END',
+}
+
+
+-- Metatable for Parser objects.
+local parser_mt = {
+ __index = {
+ -- Return the type of the current event.
+ type = function(self)
+ return tostring(self.event.type)
+ end,
+
+ -- Raise a parse error.
+ error = function(self, errmsg, ...)
+ error(format('%d:%d: ' .. errmsg, self.mark.line,
+ self.mark.column, ...), 0)
+ end,
+
+ -- Save node in the anchor table for reference in future ALIASes.
+ add_anchor = function(self, node)
+ if self.event.anchor ~= nil then
+ self.anchors[self.event.anchor] = {
+ type = alias_type[self.event.type],
+ value = node,
+ }
+ end
+ end,
+
+ -- Fetch the next event.
+ parse = function(self)
+ local ok, event = pcall(self.next)
+ if not ok then
+ -- if ok is nil, then event is a parser error from libYAML
+ self:error(gsub(event, ' at document: .*$', ''))
+ end
+ self.event = event
+ self.mark = {
+ line = self.event.start_mark.line + 1,
+ column = self.event.start_mark.column + 1,
+ }
+ return self:type()
+ end,
+
+ -- Construct a Lua hash table from following events.
+ load_map = function(self)
+ local map = {}
+ self:add_anchor(map)
+ while true do
+ local key = self:load_node()
+ local tag = self.event.tag
+ if tag then
+ tag = match(tag, '^' .. TAG_PREFIX .. '(.*)$')
+ end
+ if key == nil then
+ break
+ end
+ if key == '<<' or tag == 'merge' then
+ tag = self.event.tag or key
+ local node, event = self:load_node()
+ if event == 'MAPPING_END' then
+ for k, v in pairs(node) do
+ if map[k] == nil then
+ map[k] = v
+ end
+ end
+
+ elseif event == 'SEQUENCE_END' then
+ for i, merge in ipairs(node) do
+ if type(merge) ~= 'table' then
+ self:error("invalid '%s' sequence element %d: %s",
+ tag, i, tostring(merge))
+ end
+ for k, v in pairs(merge) do
+ if map[k] == nil then
+ map[k] = v
+ end
+ end
+ end
+
+ else
+ if event == 'SCALAR' then
+ event = tostring(node)
+ end
+ self:error("invalid '%s' merge event: %s", tag, event)
+ end
+ else
+ local value, event = self:load_node()
+ if value == nil then
+ self:error('unexpected %s event', self:type())
+ end
+ map[key] = value
+ end
+ end
+ return map, self:type()
+ end,
+
+ -- Construct a Lua array table from following events.
+ load_sequence = function(self)
+ local sequence = {}
+ self:add_anchor(sequence)
+ while true do
+ local node = self:load_node()
+ if node == nil then
+ break
+ end
+ sequence[#sequence + 1] = node
+ end
+ return sequence, self:type()
+ end,
+
+ -- Construct a primitive type from the current event.
+ load_scalar = function(self)
+ local value = self.event.value
+ local tag = self.event.tag
+ local explicit = self.explicit_scalar[tag]
+
+ -- Explicitly tagged values.
+ if explicit then
+ value = explicit(value)
+ if value == nil then
+ self:error("invalid '%s' value: '%s'", tag, self.event.value)
+ end
+
+ -- Otherwise, implicit conversion according to value content.
+ elseif self.event.style == 'PLAIN' then
+ value = self.implicit_scalar(self.event.value)
+ end
+ self:add_anchor(value)
+ return value, self:type()
+ end,
+
+ load_alias = function(self)
+ local anchor = self.event.anchor
+ local event = self.anchors[anchor]
+ if event == nil then
+ self:error('invalid reference: %s', tostring(anchor))
+ end
+ return event.value, event.type
+ end,
+
+ load_node = function(self)
+ local dispatch = {
+ SCALAR = self.load_scalar,
+ ALIAS = self.load_alias,
+ MAPPING_START = self.load_map,
+ SEQUENCE_START = self.load_sequence,
+ MAPPING_END = function() end,
+ SEQUENCE_END = function() end,
+ DOCUMENT_END = function() end,
+ }
+
+ local event = self:parse()
+ if dispatch[event] == nil then
+ self:error('invalid event: %s', self:type())
+ end
+ return dispatch[event](self)
+ end,
+ },
+}
+
+
+-- Parser object constructor.
+local function Parser(s, opts)
+ local object = {
+ anchors = {},
+ explicit_scalar = opts.explicit_scalar,
+ implicit_scalar = opts.implicit_scalar,
+ mark = {line=0, column=0},
+ next = yaml.parser(s),
+ }
+ return setmetatable(object, parser_mt)
+end
+
+
+--- Load options table.
+-- @table loader_opts
+-- @tfield boolean all load all documents from the stream
+-- @tfield table explicit_scalar map full tag-names to parser functions
+-- @tfield function implicit_scalar parse implicit scalar values
+
+
+--- Load a YAML stream into a Lua table.
+-- @tparam string s YAML stream
+-- @tparam[opt] loader_opts opts initialisation options
+-- @treturn table Lua table equivalent of stream *s*
+local function load(s, opts)
+ opts = opts or {}
+ local documents = {}
+ local all = false
+
+ -- backwards compatibility
+ if opts == true then
+ opts = {all=true}
+ end
+
+ local parser = Parser(s, {
+ explicit_scalar = opts.explicit_scalar or default.explicit_scalar,
+ implicit_scalar = opts.implicit_scalar or default.implicit_scalar,
+ })
+
+ if parser:parse() ~= 'STREAM_START' then
+ error('expecting STREAM_START event, but got ' .. parser:type(), 2)
+ end
+
+ while parser:parse() ~= 'STREAM_END' do
+ local document = parser:load_node()
+ if document == nil then
+ error('unexpected ' .. parser:type() .. ' event')
+ end
+
+ if parser:parse() ~= 'DOCUMENT_END' then
+ error('expecting DOCUMENT_END event, but got ' .. parser:type(), 2)
+ end
+
+ -- save document
+ documents[#documents + 1] = document
+
+ -- reset anchor table
+ parser.anchors = {}
+ end
+
+ return opts.all and documents or documents[1]
+end
+
+
+--[[ ----------------- ]]--
+--[[ Public Interface. ]]--
+--[[ ----------------- ]]--
+
+
+--- @export
+return {
+ dump = dump,
+ load = load,
+
+ --- `lyaml.null` value.
+ -- @table null
+ null = NULL,
+
+ --- Version number from yaml C binding.
+ -- @table _VERSION
+ _VERSION = yaml.version,
+}