summaryrefslogtreecommitdiff
path: root/macros/luatex/generic/penlight/pl/utils.lua
diff options
context:
space:
mode:
Diffstat (limited to 'macros/luatex/generic/penlight/pl/utils.lua')
-rw-r--r--macros/luatex/generic/penlight/pl/utils.lua777
1 files changed, 777 insertions, 0 deletions
diff --git a/macros/luatex/generic/penlight/pl/utils.lua b/macros/luatex/generic/penlight/pl/utils.lua
new file mode 100644
index 0000000000..66edda6db6
--- /dev/null
+++ b/macros/luatex/generic/penlight/pl/utils.lua
@@ -0,0 +1,777 @@
+--- Generally useful routines.
+-- See @{01-introduction.md.Generally_useful_functions|the Guide}.
+--
+-- Dependencies: `pl.compat`, all exported fields and functions from
+-- `pl.compat` are also available in this module.
+--
+-- @module pl.utils
+local format = string.format
+local compat = require 'pl.compat'
+local stdout = io.stdout
+local append = table.insert
+local _unpack = table.unpack -- always injected by 'compat'
+
+local is_windows = compat.is_windows
+local err_mode = 'default'
+local raise
+local operators
+local _function_factories = {}
+
+
+local utils = { _VERSION = "1.11.0" }
+for k, v in pairs(compat) do utils[k] = v end
+
+--- Some standard patterns
+-- @table patterns
+utils.patterns = {
+ FLOAT = '[%+%-%d]%d*%.?%d*[eE]?[%+%-]?%d*', -- floating point number
+ INTEGER = '[+%-%d]%d*', -- integer number
+ IDEN = '[%a_][%w_]*', -- identifier
+ FILE = '[%a%.\\][:%][%w%._%-\\]*', -- file
+}
+
+
+--- Standard meta-tables as used by other Penlight modules
+-- @table stdmt
+-- @field List the List metatable
+-- @field Map the Map metatable
+-- @field Set the Set metatable
+-- @field MultiMap the MultiMap metatable
+utils.stdmt = {
+ List = {_name='List'},
+ Map = {_name='Map'},
+ Set = {_name='Set'},
+ MultiMap = {_name='MultiMap'},
+}
+
+
+--- pack an argument list into a table.
+-- @param ... any arguments
+-- @return a table with field `n` set to the length
+-- @function utils.pack
+-- @see compat.pack
+-- @see utils.npairs
+-- @see utils.unpack
+utils.pack = table.pack -- added here to be symmetrical with unpack
+
+--- unpack a table and return its contents.
+--
+-- NOTE: this implementation differs from the Lua implementation in the way
+-- that this one DOES honor the `n` field in the table `t`, such that it is 'nil-safe'.
+-- @param t table to unpack
+-- @param[opt] i index from which to start unpacking, defaults to 1
+-- @param[opt] t index of the last element to unpack, defaults to `t.n` or `#t`
+-- @return multiple return values from the table
+-- @function utils.unpack
+-- @see compat.unpack
+-- @see utils.pack
+-- @see utils.npairs
+-- @usage
+-- local t = table.pack(nil, nil, nil, 4)
+-- local a, b, c, d = table.unpack(t) -- this `unpack` is NOT nil-safe, so d == nil
+--
+-- local a, b, c, d = utils.unpack(t) -- this is nil-safe, so d == 4
+function utils.unpack(t, i, j)
+ return _unpack(t, i or 1, j or t.n or #t)
+end
+
+--- print an arbitrary number of arguments using a format.
+-- Output will be sent to `stdout`.
+-- @param fmt The format (see `string.format`)
+-- @param ... Extra arguments for format
+function utils.printf(fmt, ...)
+ utils.assert_string(1, fmt)
+ utils.fprintf(stdout, fmt, ...)
+end
+
+--- write an arbitrary number of arguments to a file using a format.
+-- @param f File handle to write to.
+-- @param fmt The format (see `string.format`).
+-- @param ... Extra arguments for format
+function utils.fprintf(f,fmt,...)
+ utils.assert_string(2,fmt)
+ f:write(format(fmt,...))
+end
+
+do
+ local function import_symbol(T,k,v,libname)
+ local key = rawget(T,k)
+ -- warn about collisions!
+ if key and k ~= '_M' and k ~= '_NAME' and k ~= '_PACKAGE' and k ~= '_VERSION' then
+ utils.fprintf(io.stderr,"warning: '%s.%s' will not override existing symbol\n",libname,k)
+ return
+ end
+ rawset(T,k,v)
+ end
+
+ local function lookup_lib(T,t)
+ for k,v in pairs(T) do
+ if v == t then return k end
+ end
+ return '?'
+ end
+
+ local already_imported = {}
+
+ --- take a table and 'inject' it into the local namespace.
+ -- @param t The table (table), or module name (string), defaults to this `utils` module table
+ -- @param T An optional destination table (defaults to callers environment)
+ function utils.import(t,T)
+ T = T or _G
+ t = t or utils
+ if type(t) == 'string' then
+ t = require (t)
+ end
+ local libname = lookup_lib(T,t)
+ if already_imported[t] then return end
+ already_imported[t] = libname
+ for k,v in pairs(t) do
+ import_symbol(T,k,v,libname)
+ end
+ end
+end
+
+--- return either of two values, depending on a condition.
+-- @param cond A condition
+-- @param value1 Value returned if cond is truthy
+-- @param value2 Value returned if cond is falsy
+function utils.choose(cond, value1, value2)
+ return cond and value1 or value2
+end
+
+--- convert an array of values to strings.
+-- @param t a list-like table
+-- @param[opt] temp (table) buffer to use, otherwise allocate
+-- @param[opt] tostr custom tostring function, called with (value,index). Defaults to `tostring`.
+-- @return the converted buffer
+function utils.array_tostring (t,temp,tostr)
+ temp, tostr = temp or {}, tostr or tostring
+ for i = 1,#t do
+ temp[i] = tostr(t[i],i)
+ end
+ return temp
+end
+
+
+
+--- is the object of the specified type?
+-- If the type is a string, then use type, otherwise compare with metatable
+-- @param obj An object to check
+-- @param tp String of what type it should be
+-- @return boolean
+-- @usage utils.is_type("hello world", "string") --> true
+-- -- or check metatable
+-- local my_mt = {}
+-- local my_obj = setmetatable(my_obj, my_mt)
+-- utils.is_type(my_obj, my_mt) --> true
+function utils.is_type (obj,tp)
+ if type(tp) == 'string' then return type(obj) == tp end
+ local mt = getmetatable(obj)
+ return tp == mt
+end
+
+
+
+--- an iterator with indices, similar to `ipairs`, but with a range.
+-- This is a nil-safe index based iterator that will return `nil` when there
+-- is a hole in a list. To be safe ensure that table `t.n` contains the length.
+-- @tparam table t the table to iterate over
+-- @tparam[opt=1] integer i_start start index
+-- @tparam[opt=t.n or #t] integer i_end end index
+-- @tparam[opt=1] integer step step size
+-- @treturn integer index
+-- @treturn any value at index (which can be `nil`!)
+-- @see utils.pack
+-- @see utils.unpack
+-- @usage
+-- local t = utils.pack(nil, 123, nil) -- adds an `n` field when packing
+--
+-- for i, v in utils.npairs(t, 2) do -- start at index 2
+-- t[i] = tostring(t[i])
+-- end
+--
+-- -- t = { n = 3, [2] = "123", [3] = "nil" }
+function utils.npairs(t, i_start, i_end, step)
+ step = step or 1
+ if step == 0 then
+ error("iterator step-size cannot be 0", 2)
+ end
+ local i = (i_start or 1) - step
+ i_end = i_end or t.n or #t
+ if step < 0 then
+ return function()
+ i = i + step
+ if i < i_end then
+ return nil
+ end
+ return i, t[i]
+ end
+
+ else
+ return function()
+ i = i + step
+ if i > i_end then
+ return nil
+ end
+ return i, t[i]
+ end
+ end
+end
+
+
+
+--- Error handling
+-- @section Error-handling
+
+--- assert that the given argument is in fact of the correct type.
+-- @param n argument index
+-- @param val the value
+-- @param tp the type
+-- @param verify an optional verification function
+-- @param msg an optional custom message
+-- @param lev optional stack position for trace, default 2
+-- @return the validated value
+-- @raise if `val` is not the correct type
+-- @usage
+-- local param1 = assert_arg(1,"hello",'table') --> error: argument 1 expected a 'table', got a 'string'
+-- local param4 = assert_arg(4,'!@#$%^&*','string',path.isdir,'not a directory')
+-- --> error: argument 4: '!@#$%^&*' not a directory
+function utils.assert_arg (n,val,tp,verify,msg,lev)
+ if type(val) ~= tp then
+ error(("argument %d expected a '%s', got a '%s'"):format(n,tp,type(val)),lev or 2)
+ end
+ if verify and not verify(val) then
+ error(("argument %d: '%s' %s"):format(n,val,msg),lev or 2)
+ end
+ return val
+end
+
+--- process a function argument.
+-- This is used throughout Penlight and defines what is meant by a function:
+-- Something that is callable, or an operator string as defined by <code>pl.operator</code>,
+-- such as '>' or '#'. If a function factory has been registered for the type, it will
+-- be called to get the function.
+-- @param idx argument index
+-- @param f a function, operator string, or callable object
+-- @param msg optional error message
+-- @return a callable
+-- @raise if idx is not a number or if f is not callable
+function utils.function_arg (idx,f,msg)
+ utils.assert_arg(1,idx,'number')
+ local tp = type(f)
+ if tp == 'function' then return f end -- no worries!
+ -- ok, a string can correspond to an operator (like '==')
+ if tp == 'string' then
+ if not operators then operators = require 'pl.operator'.optable end
+ local fn = operators[f]
+ if fn then return fn end
+ local fn, err = utils.string_lambda(f)
+ if not fn then error(err..': '..f) end
+ return fn
+ elseif tp == 'table' or tp == 'userdata' then
+ local mt = getmetatable(f)
+ if not mt then error('not a callable object',2) end
+ local ff = _function_factories[mt]
+ if not ff then
+ if not mt.__call then error('not a callable object',2) end
+ return f
+ else
+ return ff(f) -- we have a function factory for this type!
+ end
+ end
+ if not msg then msg = " must be callable" end
+ if idx > 0 then
+ error("argument "..idx..": "..msg,2)
+ else
+ error(msg,2)
+ end
+end
+
+
+--- assert the common case that the argument is a string.
+-- @param n argument index
+-- @param val a value that must be a string
+-- @return the validated value
+-- @raise val must be a string
+-- @usage
+-- local val = 42
+-- local param2 = utils.assert_string(2, val) --> error: argument 2 expected a 'string', got a 'number'
+function utils.assert_string (n, val)
+ return utils.assert_arg(n,val,'string',nil,nil,3)
+end
+
+--- control the error strategy used by Penlight.
+-- This is a global setting that controls how `utils.raise` behaves:
+--
+-- - 'default': return `nil + error` (this is the default)
+-- - 'error': throw a Lua error
+-- - 'quit': exit the program
+--
+-- @param mode either 'default', 'quit' or 'error'
+-- @see utils.raise
+function utils.on_error (mode)
+ mode = tostring(mode)
+ if ({['default'] = 1, ['quit'] = 2, ['error'] = 3})[mode] then
+ err_mode = mode
+ else
+ -- fail loudly
+ local err = "Bad argument expected string; 'default', 'quit', or 'error'. Got '"..tostring(mode).."'"
+ if err_mode == 'default' then
+ error(err, 2) -- even in 'default' mode fail loud in this case
+ end
+ raise(err)
+ end
+end
+
+--- used by Penlight functions to return errors. Its global behaviour is controlled
+-- by `utils.on_error`.
+-- To use this function you MUST use it in conjunction with `return`, since it might
+-- return `nil + error`.
+-- @param err the error string.
+-- @see utils.on_error
+-- @usage
+-- if some_condition then
+-- return utils.raise("some condition was not met") -- MUST use 'return'!
+-- end
+function utils.raise (err)
+ if err_mode == 'default' then
+ return nil, err
+ elseif err_mode == 'quit' then
+ return utils.quit(err)
+ else
+ error(err, 2)
+ end
+end
+raise = utils.raise
+
+
+
+--- File handling
+-- @section files
+
+--- return the contents of a file as a string
+-- @param filename The file path
+-- @param is_bin open in binary mode
+-- @return file contents
+function utils.readfile(filename,is_bin)
+ local mode = is_bin and 'b' or ''
+ utils.assert_string(1,filename)
+ local f,open_err = io.open(filename,'r'..mode)
+ if not f then return raise (open_err) end
+ local res,read_err = f:read('*a')
+ f:close()
+ if not res then
+ -- Errors in io.open have "filename: " prefix,
+ -- error in file:read don't, add it.
+ return raise (filename..": "..read_err)
+ end
+ return res
+end
+
+--- write a string to a file
+-- @param filename The file path
+-- @param str The string
+-- @param is_bin open in binary mode
+-- @return true or nil
+-- @return error message
+-- @raise error if filename or str aren't strings
+function utils.writefile(filename,str,is_bin)
+ local mode = is_bin and 'b' or ''
+ utils.assert_string(1,filename)
+ utils.assert_string(2,str)
+ local f,err = io.open(filename,'w'..mode)
+ if not f then return raise(err) end
+ local ok, write_err = f:write(str)
+ f:close()
+ if not ok then
+ -- Errors in io.open have "filename: " prefix,
+ -- error in file:write don't, add it.
+ return raise (filename..": "..write_err)
+ end
+ return true
+end
+
+--- return the contents of a file as a list of lines
+-- @param filename The file path
+-- @return file contents as a table
+-- @raise error if filename is not a string
+function utils.readlines(filename)
+ utils.assert_string(1,filename)
+ local f,err = io.open(filename,'r')
+ if not f then return raise(err) end
+ local res = {}
+ for line in f:lines() do
+ append(res,line)
+ end
+ f:close()
+ return res
+end
+
+--- OS functions
+-- @section OS-functions
+
+--- execute a shell command and return the output.
+-- This function redirects the output to tempfiles and returns the content of those files.
+-- @param cmd a shell command
+-- @param bin boolean, if true, read output as binary file
+-- @return true if successful
+-- @return actual return code
+-- @return stdout output (string)
+-- @return errout output (string)
+function utils.executeex(cmd, bin)
+ local outfile = os.tmpname()
+ local errfile = os.tmpname()
+
+ if is_windows and not outfile:find(':') then
+ outfile = os.getenv('TEMP')..outfile
+ errfile = os.getenv('TEMP')..errfile
+ end
+ cmd = cmd .. " > " .. utils.quote_arg(outfile) .. " 2> " .. utils.quote_arg(errfile)
+
+ local success, retcode = utils.execute(cmd)
+ local outcontent = utils.readfile(outfile, bin)
+ local errcontent = utils.readfile(errfile, bin)
+ os.remove(outfile)
+ os.remove(errfile)
+ return success, retcode, (outcontent or ""), (errcontent or "")
+end
+
+--- Quote and escape an argument of a command.
+-- Quotes a single (or list of) argument(s) of a command to be passed
+-- to `os.execute`, `pl.utils.execute` or `pl.utils.executeex`.
+-- @param argument (string or table/list) the argument to quote. If a list then
+-- all arguments in the list will be returned as a single string quoted.
+-- @return quoted and escaped argument.
+-- @usage
+-- local options = utils.quote_arg {
+-- "-lluacov",
+-- "-e",
+-- "utils = print(require('pl.utils')._VERSION",
+-- }
+-- -- returns: -lluacov -e 'utils = print(require('\''pl.utils'\'')._VERSION'
+function utils.quote_arg(argument)
+ if type(argument) == "table" then
+ -- encode an entire table
+ local r = {}
+ for i, arg in ipairs(argument) do
+ r[i] = utils.quote_arg(arg)
+ end
+
+ return table.concat(r, " ")
+ end
+ -- only a single argument
+ if is_windows then
+ if argument == "" or argument:find('[ \f\t\v]') then
+ -- Need to quote the argument.
+ -- Quotes need to be escaped with backslashes;
+ -- additionally, backslashes before a quote, escaped or not,
+ -- need to be doubled.
+ -- See documentation for CommandLineToArgvW Windows function.
+ argument = '"' .. argument:gsub([[(\*)"]], [[%1%1\"]]):gsub([[\+$]], "%0%0") .. '"'
+ end
+
+ -- os.execute() uses system() C function, which on Windows passes command
+ -- to cmd.exe. Escape its special characters.
+ return (argument:gsub('["^<>!|&%%]', "^%0"))
+ else
+ if argument == "" or argument:find('[^a-zA-Z0-9_@%+=:,./-]') then
+ -- To quote arguments on posix-like systems use single quotes.
+ -- To represent an embedded single quote close quoted string ('),
+ -- add escaped quote (\'), open quoted string again (').
+ argument = "'" .. argument:gsub("'", [['\'']]) .. "'"
+ end
+
+ return argument
+ end
+end
+
+--- error out of this program gracefully.
+-- @param[opt] code The exit code, defaults to -`1` if omitted
+-- @param msg The exit message will be sent to `stderr` (will be formatted with the extra parameters)
+-- @param ... extra arguments for message's format'
+-- @see utils.fprintf
+-- @usage utils.quit(-1, "Error '%s' happened", "42")
+-- -- is equivalent to
+-- utils.quit("Error '%s' happened", "42") --> Error '42' happened
+function utils.quit(code, msg, ...)
+ if type(code) == 'string' then
+ utils.fprintf(io.stderr, code, msg, ...)
+ io.stderr:write('\n')
+ code = -1 -- TODO: this is odd, see the test. Which returns 255 as exit code
+ elseif msg then
+ utils.fprintf(io.stderr, msg, ...)
+ io.stderr:write('\n')
+ end
+ os.exit(code, true)
+end
+
+
+--- String functions
+-- @section string-functions
+
+--- escape any Lua 'magic' characters in a string
+-- @param s The input string
+function utils.escape(s)
+ utils.assert_string(1,s)
+ return (s:gsub('[%-%.%+%[%]%(%)%$%^%%%?%*]','%%%1'))
+end
+
+--- split a string into a list of strings separated by a delimiter.
+-- @param s The input string
+-- @param re optional A Lua string pattern; defaults to '%s+'
+-- @param plain optional If truthy don't use Lua patterns
+-- @param n optional maximum number of elements (if there are more, the last will remian un-split)
+-- @return a list-like table
+-- @raise error if s is not a string
+-- @see splitv
+function utils.split(s,re,plain,n)
+ utils.assert_string(1,s)
+ local find,sub,append = string.find, string.sub, table.insert
+ local i1,ls = 1,{}
+ if not re then re = '%s+' end
+ if re == '' then return {s} end
+ while true do
+ local i2,i3 = find(s,re,i1,plain)
+ if not i2 then
+ local last = sub(s,i1)
+ if last ~= '' then append(ls,last) end
+ if #ls == 1 and ls[1] == '' then
+ return {}
+ else
+ return ls
+ end
+ end
+ append(ls,sub(s,i1,i2-1))
+ if n and #ls == n then
+ ls[#ls] = sub(s,i1)
+ return ls
+ end
+ i1 = i3+1
+ end
+end
+
+--- split a string into a number of return values.
+-- Identical to `split` but returns multiple sub-strings instead of
+-- a single list of sub-strings.
+-- @param s the string
+-- @param re A Lua string pattern; defaults to '%s+'
+-- @param plain don't use Lua patterns
+-- @param n optional maximum number of splits
+-- @return n values
+-- @usage first,next = splitv('user=jane=doe','=', false, 2)
+-- assert(first == "user")
+-- assert(next == "jane=doe")
+-- @see split
+function utils.splitv (s,re, plain, n)
+ return _unpack(utils.split(s,re, plain, n))
+end
+
+
+--- Functional
+-- @section functional
+
+
+--- 'memoize' a function (cache returned value for next call).
+-- This is useful if you have a function which is relatively expensive,
+-- but you don't know in advance what values will be required, so
+-- building a table upfront is wasteful/impossible.
+-- @param func a function of at least one argument
+-- @return a function with at least one argument, which is used as the key.
+function utils.memoize(func)
+ local cache = {}
+ return function(k)
+ local res = cache[k]
+ if res == nil then
+ res = func(k)
+ cache[k] = res
+ end
+ return res
+ end
+end
+
+
+--- associate a function factory with a type.
+-- A function factory takes an object of the given type and
+-- returns a function for evaluating it
+-- @tab mt metatable
+-- @func fun a callable that returns a function
+function utils.add_function_factory (mt,fun)
+ _function_factories[mt] = fun
+end
+
+local function _string_lambda(f)
+ if f:find '^|' or f:find '_' then
+ local args,body = f:match '|([^|]*)|(.+)'
+ if f:find '_' then
+ args = '_'
+ body = f
+ else
+ if not args then return raise 'bad string lambda' end
+ end
+ local fstr = 'return function('..args..') return '..body..' end'
+ local fn,err = utils.load(fstr)
+ if not fn then return raise(err) end
+ fn = fn()
+ return fn
+ else
+ return raise 'not a string lambda'
+ end
+end
+
+
+--- an anonymous function as a string. This string is either of the form
+-- '|args| expression' or is a function of one argument, '_'
+-- @param lf function as a string
+-- @return a function
+-- @function utils.string_lambda
+-- @usage
+-- string_lambda '|x|x+1' (2) == 3
+-- string_lambda '_+1' (2) == 3
+utils.string_lambda = utils.memoize(_string_lambda)
+
+
+--- bind the first argument of the function to a value.
+-- @param fn a function of at least two values (may be an operator string)
+-- @param p a value
+-- @return a function such that f(x) is fn(p,x)
+-- @raise same as @{function_arg}
+-- @see func.bind1
+-- @usage local function f(msg, name)
+-- print(msg .. " " .. name)
+-- end
+--
+-- local hello = utils.bind1(f, "Hello")
+--
+-- print(hello("world")) --> "Hello world"
+-- print(hello("sunshine")) --> "Hello sunshine"
+function utils.bind1 (fn,p)
+ fn = utils.function_arg(1,fn)
+ return function(...) return fn(p,...) end
+end
+
+
+--- bind the second argument of the function to a value.
+-- @param fn a function of at least two values (may be an operator string)
+-- @param p a value
+-- @return a function such that f(x) is fn(x,p)
+-- @raise same as @{function_arg}
+-- @usage local function f(a, b, c)
+-- print(a .. " " .. b .. " " .. c)
+-- end
+--
+-- local hello = utils.bind1(f, "world")
+--
+-- print(hello("Hello", "!")) --> "Hello world !"
+-- print(hello("Bye", "?")) --> "Bye world ?"
+function utils.bind2 (fn,p)
+ fn = utils.function_arg(1,fn)
+ return function(x,...) return fn(x,p,...) end
+end
+
+
+
+
+--- Deprecation
+-- @section deprecation
+
+do
+ -- the default implementation
+ local deprecation_func = function(msg, trace)
+ if trace then
+ warn(msg, "\n", trace) -- luacheck: ignore
+ else
+ warn(msg) -- luacheck: ignore
+ end
+ end
+
+ --- Sets a deprecation warning function.
+ -- An application can override this function to support proper output of
+ -- deprecation warnings. The warnings can be generated from libraries or
+ -- functions by calling `utils.raise_deprecation`. The default function
+ -- will write to the 'warn' system (introduced in Lua 5.4, or the compatibility
+ -- function from the `compat` module for earlier versions).
+ --
+ -- Note: only applications should set/change this function, libraries should not.
+ -- @param func a callback with signature: `function(msg, trace)` both arguments are strings, the latter being optional.
+ -- @see utils.raise_deprecation
+ -- @usage
+ -- -- write to the Nginx logs with OpenResty
+ -- utils.set_deprecation_func(function(msg, trace)
+ -- ngx.log(ngx.WARN, msg, (trace and (" " .. trace) or nil))
+ -- end)
+ --
+ -- -- disable deprecation warnings
+ -- utils.set_deprecation_func()
+ function utils.set_deprecation_func(func)
+ if func == nil then
+ deprecation_func = function() end
+ else
+ utils.assert_arg(1, func, "function")
+ deprecation_func = func
+ end
+ end
+
+ --- raises a deprecation warning.
+ -- For options see the usage example below.
+ --
+ -- Note: the `opts.deprecated_after` field is the last version in which
+ -- a feature or option was NOT YET deprecated! Because when writing the code it
+ -- is quite often not known in what version the code will land. But the last
+ -- released version is usually known.
+ -- @param opts options table
+ -- @see utils.set_deprecation_func
+ -- @usage
+ -- warn("@on") -- enable Lua warnings, they are usually off by default
+ --
+ -- function stringx.islower(str)
+ -- raise_deprecation {
+ -- source = "Penlight " .. utils._VERSION, -- optional
+ -- message = "function 'islower' was renamed to 'is_lower'", -- required
+ -- version_removed = "2.0.0", -- optional
+ -- deprecated_after = "1.2.3", -- optional
+ -- no_trace = true, -- optional
+ -- }
+ -- return stringx.is_lower(str)
+ -- end
+ -- -- output: "[Penlight 1.9.2] function 'islower' was renamed to 'is_lower' (deprecated after 1.2.3, scheduled for removal in 2.0.0)"
+ function utils.raise_deprecation(opts)
+ utils.assert_arg(1, opts, "table")
+ if type(opts.message) ~= "string" then
+ error("field 'message' of the options table must be a string", 2)
+ end
+ local trace
+ if not opts.no_trace then
+ trace = debug.traceback("", 2):match("[\n%s]*(.-)$")
+ end
+ local msg
+ if opts.deprecated_after and opts.version_removed then
+ msg = (" (deprecated after %s, scheduled for removal in %s)"):format(
+ tostring(opts.deprecated_after), tostring(opts.version_removed))
+ elseif opts.deprecated_after then
+ msg = (" (deprecated after %s)"):format(tostring(opts.deprecated_after))
+ elseif opts.version_removed then
+ msg = (" (scheduled for removal in %s)"):format(tostring(opts.version_removed))
+ else
+ msg = ""
+ end
+
+ msg = opts.message .. msg
+
+ if opts.source then
+ msg = "[" .. opts.source .."] " .. msg
+ else
+ if msg:sub(1,1) == "@" then
+ -- in Lua 5.4 "@" prefixed messages are control messages to the warn system
+ error("message cannot start with '@'", 2)
+ end
+ end
+
+ deprecation_func(msg, trace)
+ end
+
+end
+
+
+return utils
+
+