From ba51dd67803a9f23ee7a42f1a89f26c19d5b7ba9 Mon Sep 17 00:00:00 2001 From: Karl Berry Date: Thu, 5 Jan 2023 21:18:54 +0000 Subject: luakeys (5jan23) git-svn-id: svn://tug.org/texlive/trunk@65468 c570f23f-e606-0410-a88d-b1316a301751 --- .../tex/luatex/luakeys/luakeys-debug.sty | 4 +- .../tex/luatex/luakeys/luakeys-debug.tex | 4 +- Master/texmf-dist/tex/luatex/luakeys/luakeys.lua | 2740 ++++++++++++-------- Master/texmf-dist/tex/luatex/luakeys/luakeys.sty | 15 +- Master/texmf-dist/tex/luatex/luakeys/luakeys.tex | 9 +- 5 files changed, 1683 insertions(+), 1089 deletions(-) (limited to 'Master/texmf-dist/tex/luatex') diff --git a/Master/texmf-dist/tex/luatex/luakeys/luakeys-debug.sty b/Master/texmf-dist/tex/luatex/luakeys/luakeys-debug.sty index ceefd6d0e2b..aa82ed78be7 100644 --- a/Master/texmf-dist/tex/luatex/luakeys/luakeys-debug.sty +++ b/Master/texmf-dist/tex/luatex/luakeys/luakeys-debug.sty @@ -1,5 +1,5 @@ %% luakeys-debug.sty -%% Copyright 2021-2022 Josef Friedrich +%% Copyright 2021-2023 Josef Friedrich % % This work may be distributed and/or modified under the % conditions of the LaTeX Project Public License, either version 1.3c @@ -17,6 +17,6 @@ % luakeys-debug.sty and luakeys-debug.tex. \NeedsTeXFormat{LaTeX2e} -\ProvidesPackage{luakeys-debug}[2022/12/23 0.11.0 Debug package for luakeys.] +\ProvidesPackage{luakeys-debug}[2023/01/05 v0.12.0 Debug package for luakeys.] \input luakeys-debug.tex diff --git a/Master/texmf-dist/tex/luatex/luakeys/luakeys-debug.tex b/Master/texmf-dist/tex/luatex/luakeys/luakeys-debug.tex index b78951779b1..0b4830f7799 100644 --- a/Master/texmf-dist/tex/luatex/luakeys/luakeys-debug.tex +++ b/Master/texmf-dist/tex/luatex/luakeys/luakeys-debug.tex @@ -1,5 +1,5 @@ %% luakeys-debug.tex -%% Copyright 2021-2022 Josef Friedrich +%% Copyright 2021-2023 Josef Friedrich % % This work may be distributed and/or modified under the % conditions of the LaTeX Project Public License, either version 1.3c @@ -18,7 +18,7 @@ \directlua { - luakeys = require('luakeys') + luakeys = require('luakeys')() } \def\luakeysdebug% diff --git a/Master/texmf-dist/tex/luatex/luakeys/luakeys.lua b/Master/texmf-dist/tex/luatex/luakeys/luakeys.lua index 7e86d729cd7..c3da56e7166 100644 --- a/Master/texmf-dist/tex/luatex/luakeys/luakeys.lua +++ b/Master/texmf-dist/tex/luatex/luakeys/luakeys.lua @@ -1,110 +1,126 @@ --- luakeys.lua --- Copyright 2021-2022 Josef Friedrich --- --- This work may be distributed and/or modified under the --- conditions of the LaTeX Project Public License, either version 1.3c --- of this license or (at your option) any later version. --- The latest version of this license is in --- http://www.latex-project.org/lppl.txt --- and version 1.3c or later is part of all distributions of LaTeX --- version 2008/05/04 or later. --- --- This work has the LPPL maintenance status `maintained'. --- --- The Current Maintainer of this work is Josef Friedrich. --- --- This work consists of the files luakeys.lua, luakeys.sty, luakeys.tex --- luakeys-debug.sty and luakeys-debug.tex. ---- A key-value parser written with Lpeg. --- --- @module luakeys +---luakeys.lua +---Copyright 2021-2023 Josef Friedrich +--- +---This work may be distributed and/or modified under the +---conditions of the LaTeX Project Public License, either version 1.3c +---of this license or (at your option) any later version. +---The latest version of this license is in +---http://www.latex-project.org/lppl.txt +---and version 1.3c or later is part of all distributions of LaTeX +---version 2008/05/04 or later. +--- +---This work has the LPPL maintenance status `maintained'. +--- +---The Current Maintainer of this work is Josef Friedrich. +--- +---This work consists of the files luakeys.lua, luakeys.sty, luakeys.tex +---luakeys-debug.sty and luakeys-debug.tex. +----A key-value parser written with Lpeg. +--- +---@module luakeys local lpeg = require('lpeg') if not tex then + ---Dummy functions for the tests. tex = { - -- Dummy function for the tests. sp = function(input) return 1234567 end, } -end -if not token then token = { set_macro = function(csname, content, global) end, } end ---- Merge two tables in the first specified table. ---- The `merge_tables` function copies all keys from the `source` table ---- to a target table. It returns the modified target table. ---- ----@see https://stackoverflow.com/a/1283608/10193818 ---- ----@param target table ----@param source table ---- ----@return table target The modified target table. -local function merge_tables(target, source) - for key, value in pairs(source) do - if type(value) == 'table' then - if type(target[key] or false) == 'table' then - merge_tables(target[key] or {}, source[key] or {}) - elseif target[key] == nil then +local utils = (function() + --- + ---Merge two tables into the first specified table. + ---The `merge_tables` function copies keys from the `source` table + ---to the `target` table. It returns the target table. + --- + ---https://stackoverflow.com/a/1283608/10193818 + --- + ---@param target table # The target table where all values are copied. + ---@param source table # The source table from which all values are copied. + ---@param overwrite? boolean # Overwrite the values in the target table if they are present (default true). + --- + ---@return table target The modified target table. + local function merge_tables(target, source, overwrite) + if overwrite == nil then + overwrite = true + end + for key, value in pairs(source) do + if type(value) == 'table' and type(target[key] or false) == + 'table' then + merge_tables(target[key] or {}, source[key] or {}, overwrite) + elseif (not overwrite and target[key] == nil) or + (overwrite and target[key] ~= value) then target[key] = value end - elseif target[key] == nil then - target[key] = value end + return target end - return target -end ----Clone a table. ---- ----@see http://lua-users.org/wiki/CopyTable ---- ----@param orig table ---- ----@return table -local function clone_table(orig) - local orig_type = type(orig) - local copy - if orig_type == 'table' then - copy = {} - for orig_key, orig_value in next, orig, nil do - copy[clone_table(orig_key)] = clone_table(orig_value) + --- + ---Clone a table, i.e. make a deep copy of the source table. + --- + ---http://lua-users.org/wiki/CopyTable + --- + ---@param source table # The source table to be cloned. + --- + ---@return table # A deep copy of the source table. + local function clone_table(source) + local copy + if type(source) == 'table' then + copy = {} + for orig_key, orig_value in next, source, nil do + copy[clone_table(orig_key)] = clone_table(orig_value) + end + setmetatable(copy, clone_table(getmetatable(source))) + else ---number, string, boolean, etc + copy = source end - setmetatable(copy, clone_table(getmetatable(orig))) - else -- number, string, boolean, etc - copy = orig + return copy end - return copy -end -local utils = { - --- Get the size of an array like table `{ 'one', 'two', 'three' }` = 3. --- - ---@param value table # A table or any input. + ---Remove an element from a table. --- - ---@return number # The size of the array like table. 0 if the input is no table or the table is empty. - get_array_size = function(value) - local count = 0 - if type(value) == 'table' then - for _ in ipairs(value) do - count = count + 1 + ---@param source table + ---@param value any + --- + ---@return any|nil + local function remove_from_table(source, value) + for index, v in pairs(source) do + if value == v then + source[index] = nil + return value end end - return count - end, + end + + --- + ---@param source table + --- + ---@return table # An array table with the sorted key names. + local function get_table_keys(source) + local keys = {} + for key in pairs(source) do + table.insert(keys, key) + end + table.sort(keys) + return keys + end + --- ---Get the size of a table `{ one = 'one', 'two', 'three' }` = 3. --- - ---@param value table|any # A table or any input. + ---@param value any # A table or any input. --- ---@return number # The size of the array like table. 0 if the input is no table or the table is empty. - get_table_size = function(value) + local function get_table_size(value) local count = 0 if type(value) == 'table' then for _ in pairs(value) do @@ -112,28 +128,34 @@ local utils = { end end return count - end, - - merge_tables = merge_tables, - - clone_table = clone_table, + end - remove_from_array = function(array, element) - for index, value in pairs(array) do - if element == value then - array[index] = nil - return value + --- + ---Get the size of an array like table, for example `{ 'one', 'two', + ---'three' }` = 3. + --- + ---@param value any # A table or any input. + --- + ---@return number # The size of the array like table. 0 if the input is no table or the table is empty. + local function get_array_size(value) + local count = 0 + if type(value) == 'table' then + for _ in ipairs(value) do + count = count + 1 end end - end, + return count + end + --- ---Scan for an optional argument. --- ---@param initial_delimiter? string # The character that marks the beginning of an optional argument (by default `[`). ---@param end_delimiter? string # The character that marks the end of an optional argument (by default `]`). --- ---@return string|nil # The string that was enclosed by the delimiters. The delimiters themselves are not returned. - scan_oarg = function(initial_delimiter, end_delimiter) + local function scan_oarg(initial_delimiter, + end_delimiter) if initial_delimiter == nil then initial_delimiter = '[' end @@ -167,11 +189,603 @@ local utils = { else token.put_next(t) end - end, -} + end + + --- + ---Throw a single error message. + --- + ---@param message string + ---@param help? table + local function throw_error_message(message, help) + if type(tex.error) == 'function' then + tex.error(message, help) + else + error(message) + end + end + + --- + ---Throw an error by specifying an error code. + --- + ---@param error_messages table + ---@param error_code string + ---@param args? table + local function throw_error_code(error_messages, + error_code, + args) + local template = error_messages[error_code] + + --- + ---@param message string + ---@param a table + --- + ---@return string + local function replace_args(message, a) + for key, value in pairs(a) do + if type(value) == 'table' then + value = table.concat(value, ', ') + end + message = message:gsub('@' .. key, + '“' .. tostring(value) .. '”') + end + return message + end + + --- + ---@param list table + ---@param a table + --- + ---@return table + local function replace_args_in_list(list, a) + for index, message in ipairs(list) do + list[index] = replace_args(message, a) + end + return list + end + + --- + ---@type string + local message + ---@type table + local help = {} + + if type(template) == 'table' then + message = template[1] + if args ~= nil then + help = replace_args_in_list(template[2], args) + else + help = template[2] + end + else + message = template + end + + if args ~= nil then + message = replace_args(message, args) + end + + message = 'luakeys error [' .. error_code .. ']: ' .. message + + for _, help_message in ipairs({ + 'You may be able to find more help in the documentation:', + 'http://mirrors.ctan.org/macros/luatex/generic/luakeys/luakeys-doc.pdf', + 'Or ask a question in the issue tracker on Github:', + 'https://github.com/Josef-Friedrich/luakeys/issues', + }) do + table.insert(help, help_message) + end + + throw_error_message(message, help) + end + + local function visit_tree(tree, callback_func) + if type(tree) ~= 'table' then + throw_error_message( + 'Parameter “tree” has to be a table, got: ' .. + tostring(tree)) + end + local function visit_tree_recursive(tree, + current, + result, + depth, + callback_func) + for key, value in pairs(current) do + if type(value) == 'table' then + value = visit_tree_recursive(tree, value, {}, depth + 1, + callback_func) + end + + key, value = callback_func(key, value, depth, current, tree) + + if key ~= nil and value ~= nil then + result[key] = value + end + end + if next(result) ~= nil then + return result + end + end + + local result = + visit_tree_recursive(tree, tree, {}, 1, callback_func) + + if result == nil then + return {} + end + return result + end + + ---@alias ColorName 'black'|'red'|'green'|'yellow'|'blue'|'magenta'|'cyan'|'white'|'reset' + ---@alias ColorMode 'bright'|'dim' + + --- + ---Small library to surround strings with ANSI color codes. + -- + ---[SGR (Select Graphic Rendition) Parameters](https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_(Select_Graphic_Rendition)_parameters) + --- + ---__attributes__ + --- + ---| color |code| + ---|------------|----| + ---| reset | 0 | + ---| clear | 0 | + ---| bright | 1 | + ---| dim | 2 | + ---| underscore | 4 | + ---| blink | 5 | + ---| reverse | 7 | + ---| hidden | 8 | + --- + ---__foreground__ + --- + ---| color |code| + ---|------------|----| + ---| black | 30 | + ---| red | 31 | + ---| green | 32 | + ---| yellow | 33 | + ---| blue | 34 | + ---| magenta | 35 | + ---| cyan | 36 | + ---| white | 37 | + --- + ---__background__ + --- + ---| color |code| + ---|------------|----| + ---| onblack | 40 | + ---| onred | 41 | + ---| ongreen | 42 | + ---| onyellow | 43 | + ---| onblue | 44 | + ---| onmagenta | 45 | + ---| oncyan | 46 | + ---| onwhite | 47 | + local ansi_color = (function() + + --- + ---@param code integer + --- + ---@return string + local function format_color_code(code) + return string.char(27) .. '[' .. tostring(code) .. 'm' + end + + --- + ---@private + --- + ---@param color ColorName # A color name. + ---@param mode? ColorMode + ---@param background? boolean # Colorize the background not the text. + --- + ---@return string + local function get_color_code(color, mode, background) + local output = '' + local code + + if mode == 'bright' then + output = format_color_code(1) + elseif mode == 'dim' then + output = format_color_code(2) + end + + if not background then + if color == 'reset' then + code = 0 + elseif color == 'black' then + code = 30 + elseif color == 'red' then + code = 31 + elseif color == 'green' then + code = 32 + elseif color == 'yellow' then + code = 33 + elseif color == 'blue' then + code = 34 + elseif color == 'magenta' then + code = 35 + elseif color == 'cyan' then + code = 36 + elseif color == 'white' then + code = 37 + else + code = 37 + end + else + if color == 'black' then + code = 40 + elseif color == 'red' then + code = 41 + elseif color == 'green' then + code = 42 + elseif color == 'yellow' then + code = 43 + elseif color == 'blue' then + code = 44 + elseif color == 'magenta' then + code = 45 + elseif color == 'cyan' then + code = 46 + elseif color == 'white' then + code = 47 + else + code = 40 + end + end + return output .. format_color_code(code) + end + + --- + ---@param text any + ---@param color ColorName # A color name. + ---@param mode? ColorMode + ---@param background? boolean # Colorize the background not the text. + --- + ---@return string + local function colorize(text, color, mode, background) + return string.format('%s%s%s', + get_color_code(color, mode, background), text, + get_color_code('reset')) + end + + return { + colorize = colorize, + + --- + ---@param text any + --- + ---@return string + red = function(text) + return colorize(text, 'red') + end, + + --- + ---@param text any + --- + ---@return string + green = function(text) + return colorize(text, 'green') + end, + + ---@return string + yellow = function(text) + return colorize(text, 'yellow') + end, + + --- + ---@param text any + --- + ---@return string + blue = function(text) + return colorize(text, 'blue') + end, + + --- + ---@param text any + --- + ---@return string + magenta = function(text) + return colorize(text, 'magenta') + end, + + --- + ---@param text any + --- + ---@return string + cyan = function(text) + return colorize(text, 'cyan') + end, + } + end)() + + --- + ---A small logging library. + --- + ---Log levels: + --- + ---* 0: silent + ---* 1: error + ---* 2: warn + ---* 3: info + ---* 4: verbose + ---* 5: debug + --- + local log = (function() + ---@private + local opts = { level = 0 } + + ---@private + local function print_message(message, ...) + print(string.format(message, ...)) + end + + --- + ---Set the log level. + --- + ---@param level 0|'silent'|1|'error'|2|'warn'|3|'info'|4|'verbose'|5|'debug' + local function set_log_level(level) + if type(level) == 'string' then + if level == 'silent' then + opts.level = 0 + elseif level == 'error' then + opts.level = 1 + elseif level == 'warn' then + opts.level = 2 + elseif level == 'info' then + opts.level = 3 + elseif level == 'verbose' then + opts.level = 4 + elseif level == 'debug' then + opts.level = 5 + else + throw_error_message(string.format('Unknown log level: %s', + level)) + end + else + if level > 5 or level < 0 then + throw_error_message(string.format( + 'Log level out of range 0-5: %s', level)) + end + opts.level = level + end + + end + + --- + ---Log at level 1 (error). + --- + ---The other log levels are: 0 (silent), 1 (error), 2 (warn), 3 (info), 4 (verbose), 5 (debug). + --- + ---@param message string + ---@param ... any + local function error(message, ...) + if opts.level >= 1 then + print_message(message, ...) + end + end + + --- + ---Log at level 2 (warn). + --- + ---The other log levels are: 0 (silent), 1 (error), 2 (warn), 3 (info), 4 (verbose), 5 (debug). + --- + ---@param message string + ---@param ... any + local function warn(message, ...) + if opts.level >= 2 then + print_message(message, ...) + end + end + + --- + ---Log at level 3 (info). + --- + ---The other log levels are: 0 (silent), 1 (error), 2 (warn), 3 (info), 4 (verbose), 5 (debug). + --- + ---@param message string + ---@param ... any + local function info(message, ...) + if opts.level >= 3 then + print_message(message, ...) + end + end + + --- + ---Log at level 4 (verbose). + --- + ---The other log levels are: 0 (silent), 1 (error), 2 (warn), 3 (info), 4 (verbose), 5 (debug). + --- + ---@param message string + ---@param ... any + local function verbose(message, ...) + if opts.level >= 4 then + print_message(message, ...) + end + end + + --- + ---Log at level 5 (debug). + --- + ---The other log levels are: 0 (silent), 1 (error), 2 (warn), 3 (info), 4 (verbose), 5 (debug). + --- + ---@param message string + ---@param ... any + local function debug(message, ...) + if opts.level >= 5 then + print_message(message, ...) + end + end + + return { + set_log_level = set_log_level, + error = error, + warn = warn, + info = info, + verbose = verbose, + debug = debug, + } + end)() + + return { + merge_tables = merge_tables, + clone_table = clone_table, + remove_from_table = remove_from_table, + get_table_keys = get_table_keys, + get_table_size = get_table_size, + get_array_size = get_array_size, + visit_tree = visit_tree, + scan_oarg = scan_oarg, + throw_error_message = throw_error_message, + throw_error_code = throw_error_code, + log = log, + ansi_color = ansi_color, + } +end)() + +--- +---Convert back to strings +---@section +local visualizers = (function() + + --- + ---The function `render(tbl)` reverses the function + ---`parse(kv_string)`. It takes a Lua table and converts this table + ---into a key-value string. The resulting string usually has a + ---different order as the input table. In Lua only tables with + ---1-based consecutive integer keys (a.k.a. array tables) can be + ---parsed in order. + --- + ---@param result table # A table to be converted into a key-value string. + --- + ---@return string # A key-value string that can be passed to a TeX macro. + local function render(result) + local function render_inner(result) + local output = {} + local function add(text) + table.insert(output, text) + end + for key, value in pairs(result) do + if (key and type(key) == 'string') then + if (type(value) == 'table') then + if (next(value)) then + add(key .. '={') + add(render_inner(value)) + add('},') + else + add(key .. '={},') + end + else + add(key .. '=' .. tostring(value) .. ',') + end + else + add(tostring(value) .. ',') + end + end + return table.concat(output) + end + return render_inner(result) + end + + --- + ---The function `stringify(tbl, for_tex)` converts a Lua table into a + ---printable string. Stringify a table means to convert the table into + ---a string. This function is used to realize the `debug` function. + ---`stringify(tbl, true)` (`for_tex = true`) generates a string which + ---can be embeded into TeX documents. The macro `\luakeysdebug{}` uses + ---this option. `stringify(tbl, false)` or `stringify(tbl)` generate a + ---string suitable for the terminal. + --- + ---@see https://stackoverflow.com/a/54593224/10193818 + --- + ---@param result table # A table to stringify. + ---@param for_tex? boolean # Stringify the table into a text string that can be embeded inside a TeX document via tex.print(). Curly braces and whites spaces are escaped. + --- + ---@return string + local function stringify(result, for_tex) + local line_break, start_bracket, end_bracket, indent + + if for_tex then + line_break = '\\par' + start_bracket = '$\\{$' + end_bracket = '$\\}$' + indent = '\\ \\ ' + else + line_break = '\n' + start_bracket = '{' + end_bracket = '}' + indent = ' ' + end + + local function stringify_inner(input, depth) + local output = {} + depth = depth or 0 + + local function add(depth, text) + table.insert(output, string.rep(indent, depth) .. text) + end + + local function format_key(key) + if (type(key) == 'number') then + return string.format('[%s]', key) + else + return string.format('[\'%s\']', key) + end + end + + if type(input) ~= 'table' then + return tostring(input) + end + + for key, value in pairs(input) do + if (key and type(key) == 'number' or type(key) == 'string') then + key = format_key(key) + + if (type(value) == 'table') then + if (next(value)) then + add(depth, key .. ' = ' .. start_bracket) + add(0, stringify_inner(value, depth + 1)) + add(depth, end_bracket .. ','); + else + add(depth, + key .. ' = ' .. start_bracket .. end_bracket .. ',') + end + else + if (type(value) == 'string') then + value = string.format('\'%s\'', value) + else + value = tostring(value) + end + + add(depth, key .. ' = ' .. value .. ',') + end + end + end + + return table.concat(output, line_break) + end + + return start_bracket .. line_break .. stringify_inner(result, 1) .. + line_break .. end_bracket + end + + --- + ---The function `debug(result)` pretty prints a Lua table to standard + ---output (stdout). It is a utility function that can be used to + ---debug and inspect the resulting Lua table of the function + ---`parse`. You have to compile your TeX document in a console to + ---see the terminal output. + --- + ---@param result table # A table to be printed to standard output for debugging purposes. + local function debug(result) + print('\n' .. stringify(result, false)) + end + + return { render = render, stringify = stringify, debug = debug } +end)() local namespace = { opts = { + accumulated_result = false, assignment_operator = '=', convert_dimensions = false, debug = false, @@ -209,6 +823,7 @@ local namespace = { choices = true, data_type = true, default = true, + description = true, exclusive_group = true, l3_tl_set = true, macro = true, @@ -220,1151 +835,1116 @@ local namespace = { required = true, sub_keys = true, }, + + error_messages = { + E001 = { + 'Unknown parse option: @unknown!', + { 'The available options are:', '@opt_names' }, + }, + E002 = { + 'Unknown hook: @unknown!', + { 'The available hooks are:', '@hook_names' }, + }, + E003 = 'Duplicate aliases @alias1 and @alias2 for key @key!', + E004 = 'The value @value does not exist in the choices: @choices', + E005 = 'Unknown data type: @unknown', + E006 = 'The value @value of the key @key could not be converted into the data type @data_type!', + E007 = 'The key @key belongs to the mutually exclusive group @exclusive_group and another key of the group named @another_key is already present!', + E008 = 'def.match has to be a string', + E009 = 'The value @value of the key @key does not match @match!', + + E011 = 'Wrong data type in the “pick” attribute: @unknown. Allowed are: @data_types.', + E012 = 'Missing required key @key!', + E013 = 'The key definition must be a table! Got @data_type for key @key.', + E014 = { + 'Unknown definition attribute: @unknown', + { 'The available attributes are:', '@attr_names' }, + }, + E015 = 'Key name couldn’t be detected!', + E017 = 'Unknown style to format keys: @unknown! Allowed styles are: @styles', + E018 = 'The option “format_keys” has to be a table not @data_type', + E019 = 'Unknown keys: @unknown', + + ---Input / parsing error + E021 = 'Opposite key was specified more than once: @key!', + E020 = 'Both opposite keys were given: @true and @false!', + ---Config error (wrong configuration of luakeys) + E010 = 'Usage: opposite_keys = { "true_key", "false_key" } or { [true] = "true_key", [false] = "false_key" } ', + E023 = { + 'Don’t use this function from the global luakeys table. Create a new instance using e. g.: local lk = luakeys.new()', + { + 'This functions should not be used from the global luakeys table:', + 'parse()', + 'save()', + 'get()', + }, + }, + }, } ---- The default options. -local default_options = clone_table(namespace.opts) +--- +---@return table # The public interface of the module. +local function main() -local function throw_error(message) - if type(tex.error) == 'function' then - tex.error(message) - else - error(message) - end -end + ---The default options. + local default_opts = utils.clone_table(namespace.opts) ---- Normalize the parse options. ---- ----@param opts? table # Options in a raw format. The table may be empty or some keys are not set. ---- ----@return table -local function normalize_opts(opts) - if type(opts) ~= 'table' then - opts = {} + local error_messages = utils.clone_table(namespace.error_messages) + + --- + ---@param error_code string + ---@param args? table + local function throw_error(error_code, args) + utils.throw_error_code(error_messages, error_code, args) end - for key, _ in pairs(opts) do - if namespace.opts[key] == nil then - throw_error('Unknown parse option: ' .. tostring(key) .. '!') + + --- + ---Normalize the parse options. + --- + ---@param opts? table # Options in a raw format. The table may be empty or some keys are not set. + --- + ---@return table + local function normalize_opts(opts) + if type(opts) ~= 'table' then + opts = {} end - end - local old_opts = opts - opts = {} - for name, _ in pairs(namespace.opts) do - if old_opts[name] ~= nil then - opts[name] = old_opts[name] - else - opts[name] = default_options[name] + for key, _ in pairs(opts) do + if namespace.opts[key] == nil then + throw_error('E001', { + unknown = key, + opt_names = utils.get_table_keys(namespace.opts), + }) + end + end + local old_opts = opts + opts = {} + for name, _ in pairs(namespace.opts) do + if old_opts[name] ~= nil then + opts[name] = old_opts[name] + else + opts[name] = default_opts[name] + end end - end - for hook in pairs(opts.hooks) do - if namespace.hooks[hook] == nil then - throw_error('Unknown hook: ' .. tostring(hook) .. '!') + for hook in pairs(opts.hooks) do + if namespace.hooks[hook] == nil then + throw_error('E002', { + unknown = hook, + hook_names = utils.get_table_keys(namespace.hooks), + }) + end end + return opts end - return opts -end -local l3_code_cctab = 10 + local l3_code_cctab = 10 ---- Convert back to strings --- @section + --- + ---Parser / Lpeg related + ---@section ---- The function `render(tbl)` reverses the function ---- `parse(kv_string)`. It takes a Lua table and converts this table ---- into a key-value string. The resulting string usually has a ---- different order as the input table. In Lua only tables with ---- 1-based consecutive integer keys (a.k.a. array tables) can be ---- parsed in order. ---- ----@param result table # A table to be converted into a key-value string. ---- ----@return string # A key-value string that can be passed to a TeX macro. -local function render(result) - local function render_inner(result) - local output = {} - local function add(text) - table.insert(output, text) + ---Generate the PEG parser using Lpeg. + --- + ---Explanations of some LPeg notation forms: + --- + ---* `patt ^ 0` = `expression *` + ---* `patt ^ 1` = `expression +` + ---* `patt ^ -1` = `expression ?` + ---* `patt1 * patt2` = `expression1 expression2`: Sequence + ---* `patt1 + patt2` = `expression1 / expression2`: Ordered choice + --- + ---* [TUGboat article: Parsing complex data formats in LuaTEX with LPEG](https://tug.or-g/TUGboat/tb40-2/tb125menke-Patterndf) + --- + ---@param initial_rule string # The name of the first rule of the grammar table passed to the `lpeg.P(attern)` function (e. g. `list`, `number`). + ---@param opts? table # Whether the dimensions should be converted to scaled points (by default `false`). + --- + ---@return userdata # The parser. + local function generate_parser(initial_rule, opts) + if type(opts) ~= 'table' then + opts = normalize_opts(opts) end - for key, value in pairs(result) do - if (key and type(key) == 'string') then - if (type(value) == 'table') then - if (next(value)) then - add(key .. '={') - add(render_inner(value)) - add('},') - else - add(key .. '={},') - end - else - add(key .. '=' .. tostring(value) .. ',') - end - else - add(tostring(value) .. ',') - end - end - return table.concat(output) - end - return render_inner(result) -end - ---- The function `stringify(tbl, for_tex)` converts a Lua table into a ---- printable string. Stringify a table means to convert the table into ---- a string. This function is used to realize the `debug` function. ---- `stringify(tbl, true)` (`for_tex = true`) generates a string which ---- can be embeded into TeX documents. The macro `\luakeysdebug{}` uses ---- this option. `stringify(tbl, false)` or `stringify(tbl)` generate a ---- string suitable for the terminal. ---- ----@see https://stackoverflow.com/a/54593224/10193818 ---- ----@param result table # A table to stringify. ----@param for_tex? boolean # Stringify the table into a text string that can be embeded inside a TeX document via tex.print(). Curly braces and whites spaces are escaped. ---- ----@return string -local function stringify(result, for_tex) - local line_break, start_bracket, end_bracket, indent - - if for_tex then - line_break = '\\par' - start_bracket = '$\\{$' - end_bracket = '$\\}$' - indent = '\\ \\ ' - else - line_break = '\n' - start_bracket = '{' - end_bracket = '}' - indent = ' ' - end - - local function stringify_inner(input, depth) - local output = {} - depth = depth or 0 - local function add(depth, text) - table.insert(output, string.rep(indent, depth) .. text) + local Variable = lpeg.V + local Pattern = lpeg.P + local Set = lpeg.S + local Range = lpeg.R + local CaptureGroup = lpeg.Cg + local CaptureFolding = lpeg.Cf + local CaptureTable = lpeg.Ct + local CaptureConstant = lpeg.Cc + local CaptureSimple = lpeg.C + + ---Optional whitespace + local white_space = Set(' \t\n\r') + + ---Match literal string surrounded by whitespace + local ws = function(match) + return white_space ^ 0 * Pattern(match) * white_space ^ 0 end - local function format_key(key) - if (type(key) == 'number') then - return string.format('[%s]', key) - else - return string.format('[\'%s\']', key) - end - end - - if type(input) ~= 'table' then - return tostring(input) - end - - for key, value in pairs(input) do - if (key and type(key) == 'number' or type(key) == 'string') then - key = format_key(key) - - if (type(value) == 'table') then - if (next(value)) then - add(depth, key .. ' = ' .. start_bracket) - add(0, stringify_inner(value, depth + 1)) - add(depth, end_bracket .. ','); - else - add(depth, - key .. ' = ' .. start_bracket .. end_bracket .. ',') - end + local line_up_pattern = function(patterns) + local result + for _, pattern in ipairs(patterns) do + if result == nil then + result = Pattern(pattern) else - if (type(value) == 'string') then - value = string.format('\'%s\'', value) - else - value = tostring(value) - end - - add(depth, key .. ' = ' .. value .. ',') + result = result + Pattern(pattern) end end + return result end - return table.concat(output, line_break) - end - - return start_bracket .. line_break .. stringify_inner(result, 1) .. - line_break .. end_bracket -end - ---- The function `debug(result)` pretty prints a Lua table to standard --- output (stdout). It is a utility function that can be used to --- debug and inspect the resulting Lua table of the function --- `parse`. You have to compile your TeX document in a console to --- see the terminal output. --- ----@param result table # A table to be printed to standard output for debugging purposes. -local function debug(result) - print('\n' .. stringify(result, false)) -end - ---- Parser / Lpeg related --- @section - ---- Generate the PEG parser using Lpeg. ---- ---- Explanations of some LPeg notation forms: ---- ---- * `patt ^ 0` = `expression *` ---- * `patt ^ 1` = `expression +` ---- * `patt ^ -1` = `expression ?` ---- * `patt1 * patt2` = `expression1 expression2`: Sequence ---- * `patt1 + patt2` = `expression1 / expression2`: Ordered choice ---- ---- * [TUGboat article: Parsing complex data formats in LuaTEX with LPEG](https://tug.or-g/TUGboat/tb40-2/tb125menke-Patterndf) ---- ----@param initial_rule string # The name of the first rule of the grammar table passed to the `lpeg.P(attern)` function (e. g. `list`, `number`). ----@param opts? table # Whether the dimensions should be converted to scaled points (by default `false`). ---- ----@return userdata # The parser. -local function generate_parser(initial_rule, opts) - if type(opts) ~= 'table' then - opts = normalize_opts(opts) - end - - local Variable = lpeg.V - local Pattern = lpeg.P - local Set = lpeg.S - local Range = lpeg.R - local CaptureGroup = lpeg.Cg - local CaptureFolding = lpeg.Cf - local CaptureTable = lpeg.Ct - local CaptureConstant = lpeg.Cc - local CaptureSimple = lpeg.C - - -- Optional whitespace - local white_space = Set(' \t\n\r') - - --- Match literal string surrounded by whitespace - local ws = function(match) - return white_space ^ 0 * Pattern(match) * white_space ^ 0 - end - - local line_up_pattern = function(patterns) - local result - for _, pattern in ipairs(patterns) do - if result == nil then - result = Pattern(pattern) + --- + ---Convert a dimension to an normalized dimension string or an + ---integer in the scaled points format. + --- + ---@param input string + --- + ---@return integer|string # A dimension as an integer or a dimension string. + local capture_dimension = function(input) + ---Remove all whitespaces + input = input:gsub('%s+', '') + ---Convert the unit string into lowercase. + input = input:lower() + if opts.convert_dimensions then + return tex.sp(input) else - result = result + Pattern(pattern) + return input end end - return result - end - --- Convert a dimension to an normalized dimension string or an - --- integer in the scaled points format. - --- - ---@param input string - --- - ---@return integer|string # A dimension as an integer or a dimension string. - local capture_dimension = function(input) - -- Remove all whitespaces - input = input:gsub('%s+', '') - -- Convert the unit string into lowercase. - input = input:lower() - if opts.convert_dimensions then - return tex.sp(input) - else - return input + --- + ---Add values to a table in two modes: + --- + ---Key-value pair: + --- + ---If `arg1` and `arg2` are not nil, then `arg1` is the key and `arg2` is the + ---value of a new table entry. + --- + ---Indexed value: + --- + ---If `arg2` is nil, then `arg1` is the value and is added as an indexed + ---(by an integer) value. + --- + ---@param result table # The result table to which an additional key-value pair or value should to be added + ---@param arg1 any # The key or the value. + ---@param arg2? any # Always the value. + --- + ---@return table # The result table to which an additional key-value pair or value has been added. + local add_to_table = function(result, arg1, arg2) + if arg2 == nil then + local index = #result + 1 + return rawset(result, index, arg1) + else + return rawset(result, arg1, arg2) + end end - end - --- Add values to a table in two modes: - -- - -- Key-value pair: - -- - -- If `arg1` and `arg2` are not nil, then `arg1` is the key and `arg2` is the - -- value of a new table entry. - -- - -- Indexed value: - -- - -- If `arg2` is nil, then `arg1` is the value and is added as an indexed - -- (by an integer) value. - -- - ---@param result table # The result table to which an additional key-value pair or value should to be added - ---@param arg1 any # The key or the value. - ---@param arg2? any # Always the value. - --- - ---@return table # The result table to which an additional key-value pair or value has been added. - local add_to_table = function(result, arg1, arg2) - if arg2 == nil then - local index = #result + 1 - return rawset(result, index, arg1) - else - return rawset(result, arg1, arg2) - end - end + -- LuaFormatter off + return Pattern({ + [1] = initial_rule, - -- LuaFormatter off - return Pattern({ - [1] = initial_rule, - - -- list_item* - list = CaptureFolding( - CaptureTable('') * Variable('list_item')^0, - add_to_table - ), - - -- '{' list '}' - list_container = - ws(opts.group_begin) * Variable('list') * ws(opts.group_end), - - -- ( list_container / key_value_pair / value ) ','? - list_item = - CaptureGroup( - Variable('list_container') + - Variable('key_value_pair') + - Variable('value') - ) * ws(opts.list_separator)^-1, - - -- key '=' (list_container / value) - key_value_pair = - (Variable('key') * ws(opts.assignment_operator)) * (Variable('list_container') + Variable('value')), - - -- number / string_quoted / string_unquoted - key = - Variable('number') + - Variable('string_quoted') + - Variable('string_unquoted'), - - -- boolean !value / dimension !value / number !value / string_quoted !value / string_unquoted - -- !value -> Not-predicate -> * -Variable('value') - value = - Variable('boolean') * -Variable('value') + - Variable('dimension') * -Variable('value') + - Variable('number') * -Variable('value') + - Variable('string_quoted') * -Variable('value') + - Variable('string_unquoted'), - - -- for is.boolean() - boolean_only = Variable('boolean') * -1, - - -- boolean_true / boolean_false - boolean = - ( - Variable('boolean_true') * CaptureConstant(true) + - Variable('boolean_false') * CaptureConstant(false) + ---list_item* + list = CaptureFolding( + CaptureTable('') * Variable('list_item')^0, + add_to_table ), - boolean_true = line_up_pattern(opts.true_aliases), - - boolean_false = line_up_pattern(opts.false_aliases), - - -- for is.dimension() - dimension_only = Variable('dimension') * -1, - - dimension = ( - Variable('tex_number') * white_space^0 * - Variable('unit') - ) / capture_dimension, - - -- for is.number() - number_only = Variable('number') * -1, - - -- capture number - number = Variable('tex_number') / tonumber, - - -- sign? white_space? (integer+ fractional? / fractional) - tex_number = - Variable('sign')^0 * white_space^0 * - (Variable('integer')^1 * Variable('fractional')^0) + - Variable('fractional'), - - sign = Set('-+'), - - fractional = Pattern('.') * Variable('integer')^1, - - integer = Range('09')^1, - - -- 'bp' / 'BP' / 'cc' / etc. - -- https://raw.githubusercontent.com/latex3/lualibs/master/lualibs-util-dim.lua - -- https://github.com/TeX-Live/luatex/blob/51db1985f5500dafd2393aa2e403fefa57d3cb76/source/texk/web2c/luatexdir/lua/ltexlib.c#L434-L625 - unit = - Pattern('bp') + Pattern('BP') + - Pattern('cc') + Pattern('CC') + - Pattern('cm') + Pattern('CM') + - Pattern('dd') + Pattern('DD') + - Pattern('em') + Pattern('EM') + - Pattern('ex') + Pattern('EX') + - Pattern('in') + Pattern('IN') + - Pattern('mm') + Pattern('MM') + - Pattern('mu') + Pattern('MU') + - Pattern('nc') + Pattern('NC') + - Pattern('nd') + Pattern('ND') + - Pattern('pc') + Pattern('PC') + - Pattern('pt') + Pattern('PT') + - Pattern('px') + Pattern('PX') + - Pattern('sp') + Pattern('SP'), - - -- '"' ('\"' / !'"')* '"' - string_quoted = - white_space^0 * Pattern(opts.quotation_begin) * - CaptureSimple((Pattern('\\' .. opts.quotation_end) + 1 - Pattern(opts.quotation_end))^0) * - Pattern(opts.quotation_end) * white_space^0, - - string_unquoted = - white_space^0 * - CaptureSimple( - Variable('word_unquoted')^1 * - (Set(' \t')^1 * Variable('word_unquoted')^1)^0) * - white_space^0, - - word_unquoted = (1 - white_space - Set( - opts.group_begin .. - opts.group_end .. - opts.assignment_operator .. - opts.list_separator))^1 - }) + ---'{' list '}' + list_container = + ws(opts.group_begin) * Variable('list') * ws(opts.group_end), + + ---( list_container / key_value_pair / value ) ','? + list_item = + CaptureGroup( + Variable('list_container') + + Variable('key_value_pair') + + Variable('value') + ) * ws(opts.list_separator)^-1, + + ---key '=' (list_container / value) + key_value_pair = + (Variable('key') * ws(opts.assignment_operator)) * (Variable('list_container') + Variable('value')), + + ---number / string_quoted / string_unquoted + key = + Variable('number') + + Variable('string_quoted') + + Variable('string_unquoted'), + + ---boolean !value / dimension !value / number !value / string_quoted !value / string_unquoted + ---!value -> Not-predicate -> * -Variable('value') + value = + Variable('boolean') * -Variable('value') + + Variable('dimension') * -Variable('value') + + Variable('number') * -Variable('value') + + Variable('string_quoted') * -Variable('value') + + Variable('string_unquoted'), + + ---for is.boolean() + boolean_only = Variable('boolean') * -1, + + ---boolean_true / boolean_false + boolean = + ( + Variable('boolean_true') * CaptureConstant(true) + + Variable('boolean_false') * CaptureConstant(false) + ), + + boolean_true = line_up_pattern(opts.true_aliases), + + boolean_false = line_up_pattern(opts.false_aliases), + + ---for is.dimension() + dimension_only = Variable('dimension') * -1, + + dimension = ( + Variable('tex_number') * white_space^0 * + Variable('unit') + ) / capture_dimension, + + ---for is.number() + number_only = Variable('number') * -1, + + ---capture number + number = Variable('tex_number') / tonumber, + + ---sign? white_space? (integer+ fractional? / fractional) + tex_number = + Variable('sign')^0 * white_space^0 * + (Variable('integer')^1 * Variable('fractional')^0) + + Variable('fractional'), + + sign = Set('-+'), + + fractional = Pattern('.') * Variable('integer')^1, + + integer = Range('09')^1, + + ---'bp' / 'BP' / 'cc' / etc. + ---https://raw.githubusercontent.com/latex3/lualibs/master/lualibs-util-dim.lua + ---https://github.com/TeX-Live/luatex/blob/51db1985f5500dafd2393aa2e403fefa57d3cb76/source/texk/web2c/luatexdir/lua/ltexlib.c#L434-L625 + unit = + Pattern('bp') + Pattern('BP') + + Pattern('cc') + Pattern('CC') + + Pattern('cm') + Pattern('CM') + + Pattern('dd') + Pattern('DD') + + Pattern('em') + Pattern('EM') + + Pattern('ex') + Pattern('EX') + + Pattern('in') + Pattern('IN') + + Pattern('mm') + Pattern('MM') + + Pattern('mu') + Pattern('MU') + + Pattern('nc') + Pattern('NC') + + Pattern('nd') + Pattern('ND') + + Pattern('pc') + Pattern('PC') + + Pattern('pt') + Pattern('PT') + + Pattern('px') + Pattern('PX') + + Pattern('sp') + Pattern('SP'), + + ---'"' ('\"' / !'"')* '"' + string_quoted = + white_space^0 * Pattern(opts.quotation_begin) * + CaptureSimple((Pattern('\\' .. opts.quotation_end) + 1 - Pattern(opts.quotation_end))^0) * + Pattern(opts.quotation_end) * white_space^0, + + string_unquoted = + white_space^0 * + CaptureSimple( + Variable('word_unquoted')^1 * + (Set(' \t')^1 * Variable('word_unquoted')^1)^0) * + white_space^0, + + word_unquoted = (1 - white_space - Set( + opts.group_begin .. + opts.group_end .. + opts.assignment_operator .. + opts.list_separator))^1 + }) -- LuaFormatter on -end - -local function visit_tree(tree, callback_func) - if type(tree) ~= 'table' then - throw_error('Parameter “tree” has to be a table, got: ' .. - tostring(tree)) end - local function visit_tree_recursive(tree, - current, - result, - depth, - callback_func) - for key, value in pairs(current) do - if type(value) == 'table' then - value = visit_tree_recursive(tree, value, {}, depth + 1, - callback_func) + + local is = { + boolean = function(value) + if value == nil then + return false end + if type(value) == 'boolean' then + return true + end + local parser = generate_parser('boolean_only') + local result = parser:match(tostring(value)) + return result ~= nil + end, - key, value = callback_func(key, value, depth, current, tree) + dimension = function(value) + if value == nil then + return false + end + local parser = generate_parser('dimension_only') + local result = parser:match(tostring(value)) + return result ~= nil + end, - if key ~= nil and value ~= nil then - result[key] = value + integer = function(value) + local n = tonumber(value) + if n == nil then + return false end - end - if next(result) ~= nil then - return result - end - end + return n == math.floor(n) + end, - local result = visit_tree_recursive(tree, tree, {}, 1, callback_func) + number = function(value) + if value == nil then + return false + end + if type(value) == 'number' then + return true + end + local parser = generate_parser('number_only') + local result = parser:match(tostring(value)) + return result ~= nil + end, - if result == nil then - return {} - end - return result -end + string = function(value) + return type(value) == 'string' + end, -local is = { - boolean = function(value) - if value == nil then - return false - end - if type(value) == 'boolean' then - return true - end - local parser = generate_parser('boolean_only') - local result = parser:match(tostring(value)) - return result ~= nil - end, - - dimension = function(value) - if value == nil then - return false - end - local parser = generate_parser('dimension_only') - local result = parser:match(tostring(value)) - return result ~= nil - end, - - integer = function(value) - local n = tonumber(value) - if n == nil then - return false - end - return n == math.floor(n) - end, + list = function(value) + if type(value) ~= 'table' then + return false + end - number = function(value) - if value == nil then - return false - end - if type(value) == 'number' then + for k, _ in pairs(value) do + if type(k) ~= 'number' then + return false + end + end return true - end - local parser = generate_parser('number_only') - local result = parser:match(tostring(value)) - return result ~= nil - end, - - string = function(value) - return type(value) == 'string' - end, - - any = function(value) - return true - end, -} - ---- Apply the key-value-pair definitions (defs) on an input table in a ---- recursive fashion. ---- ----@param defs table # A table containing all definitions. ----@param opts table # The parse options table. ----@param input table # The current input table. ----@param output table # The current output table. ----@param unknown table # Always the root unknown table. ----@param key_path table # An array of key names leading to the current ----@param input_root table # The root input table input and output table. -local function apply_definitions(defs, - opts, - input, - output, - unknown, - key_path, - input_root) - local exclusive_groups = {} - - local function add_to_key_path(key_path, key) - local new_key_path = {} - - for index, value in ipairs(key_path) do - new_key_path[index] = value - end + end, - table.insert(new_key_path, key) - return new_key_path - end + any = function(value) + return true + end, + } - local function get_default_value(def) - if def.default ~= nil then - return def.default - elseif opts ~= nil and opts.default ~= nil then - return opts.default - end - return true - end + --- + ---Apply the key-value-pair definitions (defs) on an input table in a + ---recursive fashion. + --- + ---@param defs table # A table containing all definitions. + ---@param opts table # The parse options table. + ---@param input table # The current input table. + ---@param output table # The current output table. + ---@param unknown table # Always the root unknown table. + ---@param key_path table # An array of key names leading to the current + ---@param input_root table # The root input table input and output table. + local function apply_definitions(defs, + opts, + input, + output, + unknown, + key_path, + input_root) + local exclusive_groups = {} + + local function add_to_key_path(key_path, key) + local new_key_path = {} + + for index, value in ipairs(key_path) do + new_key_path[index] = value + end - local function find_value(search_key, def) - if input[search_key] ~= nil then - local value = input[search_key] - input[search_key] = nil - return value - -- naked keys: values with integer keys - elseif utils.remove_from_array(input, search_key) ~= nil then - return get_default_value(def) + table.insert(new_key_path, key) + return new_key_path end - end - local apply = { - alias = function(value, key, def) - if type(def.alias) == 'string' then - def.alias = { def.alias } + local function get_default_value(def) + if def.default ~= nil then + return def.default + elseif opts ~= nil and opts.default ~= nil then + return opts.default end - local alias_value - local used_alias_key - -- To get an error if the key and an alias is present - if value ~= nil then - alias_value = value - used_alias_key = key - end - for _, alias in ipairs(def.alias) do - local v = find_value(alias, def) - if v ~= nil then - if alias_value ~= nil then - throw_error(string.format( - 'Duplicate aliases “%s” and “%s” for key “%s”!', - used_alias_key, alias, key)) - end - used_alias_key = alias - alias_value = v - end - end - if alias_value ~= nil then - return alias_value - end - end, + return true + end - always_present = function(value, key, def) - if value == nil and def.always_present then + local function find_value(search_key, def) + if input[search_key] ~= nil then + local value = input[search_key] + input[search_key] = nil + return value + ---naked keys: values with integer keys + elseif utils.remove_from_table(input, search_key) ~= nil then return get_default_value(def) end - end, + end - choices = function(value, key, def) - if value == nil then - return - end - if def.choices ~= nil and type(def.choices) == 'table' then - local is_in_choices = false - for _, choice in ipairs(def.choices) do - if value == choice then - is_in_choices = true + local apply = { + alias = function(value, key, def) + if type(def.alias) == 'string' then + def.alias = { def.alias } + end + local alias_value + local used_alias_key + ---To get an error if the key and an alias is present + if value ~= nil then + alias_value = value + used_alias_key = key + end + for _, alias in ipairs(def.alias) do + local v = find_value(alias, def) + if v ~= nil then + if alias_value ~= nil then + throw_error('E003', { + alias1 = used_alias_key, + alias2 = alias, + key = key, + }) + end + used_alias_key = alias + alias_value = v end end - if not is_in_choices then - throw_error('The value “' .. value .. - '” does not exist in the choices: ' .. - table.concat(def.choices, ', ') .. '!') + if alias_value ~= nil then + return alias_value end - end - end, + end, - data_type = function(value, key, def) - if value == nil then - return - end - if def.data_type ~= nil then - local converted - -- boolean - if def.data_type == 'boolean' then - if value == 0 or value == '' or not value then - converted = false - else - converted = true + always_present = function(value, key, def) + if value == nil and def.always_present then + return get_default_value(def) + end + end, + + choices = function(value, key, def) + if value == nil then + return + end + if def.choices ~= nil and type(def.choices) == 'table' then + local is_in_choices = false + for _, choice in ipairs(def.choices) do + if value == choice then + is_in_choices = true + end end - -- dimension - elseif def.data_type == 'dimension' then - if is.dimension(value) then - converted = value + if not is_in_choices then + throw_error('E004', { value = value, choices = def.choices }) end - -- integer - elseif def.data_type == 'integer' then - if is.number(value) then - local n = tonumber(value) - if type(n) == 'number' and n ~= nil then - converted = math.floor(n) + end + end, + + data_type = function(value, key, def) + if value == nil then + return + end + if def.data_type ~= nil then + local converted + ---boolean + if def.data_type == 'boolean' then + if value == 0 or value == '' or not value then + converted = false + else + converted = true + end + ---dimension + elseif def.data_type == 'dimension' then + if is.dimension(value) then + converted = value + end + ---integer + elseif def.data_type == 'integer' then + if is.number(value) then + local n = tonumber(value) + if type(n) == 'number' and n ~= nil then + converted = math.floor(n) + end + end + ---number + elseif def.data_type == 'number' then + if is.number(value) then + converted = tonumber(value) + end + ---string + elseif def.data_type == 'string' then + converted = tostring(value) + ---list + elseif def.data_type == 'list' then + if is.list(value) then + converted = value end + else + throw_error('E005', { data_type = def.data_type }) end - -- number - elseif def.data_type == 'number' then - if is.number(value) then - converted = tonumber(value) + if converted == nil then + throw_error('E006', { + value = value, + key = key, + data_type = def.data_type, + }) + else + return converted end - -- string - elseif def.data_type == 'string' then - converted = tostring(value) - else - throw_error('Unknown data type: ' .. def.data_type) - end - if converted == nil then - throw_error( - 'The value “' .. value .. '” of the key “' .. key .. - '” could not be converted into the data type “' .. - def.data_type .. '”!') - else - return converted end - end - end, + end, - exclusive_group = function(value, key, def) - if value == nil then - return - end - if def.exclusive_group ~= nil then - if exclusive_groups[def.exclusive_group] ~= nil then - throw_error('The key “' .. key .. - '” belongs to a mutually exclusive group “' .. - def.exclusive_group .. '” and the key “' .. - exclusive_groups[def.exclusive_group] .. - '” is already present!') - else - exclusive_groups[def.exclusive_group] = key + exclusive_group = function(value, key, def) + if value == nil then + return end - end - end, - - l3_tl_set = function(value, key, def) - if value == nil then - return - end - if def.l3_tl_set ~= nil then - tex.print(l3_code_cctab, - '\\tl_set:Nn \\g_' .. def.l3_tl_set .. '_tl') - tex.print('{' .. value .. '}') - end - end, + if def.exclusive_group ~= nil then + if exclusive_groups[def.exclusive_group] ~= nil then + throw_error('E007', { + key = key, + exclusive_group = def.exclusive_group, + another_key = exclusive_groups[def.exclusive_group], + }) + else + exclusive_groups[def.exclusive_group] = key + end + end + end, - macro = function(value, key, def) - if value == nil then - return - end - if def.macro ~= nil then - token.set_macro(def.macro, value, 'global') - end - end, + l3_tl_set = function(value, key, def) + if value == nil then + return + end + if def.l3_tl_set ~= nil then + tex.print(l3_code_cctab, + '\\tl_set:Nn \\g_' .. def.l3_tl_set .. '_tl') + tex.print('{' .. value .. '}') + end + end, - match = function(value, key, def) - if value == nil then - return - end - if def.match ~= nil then - if type(def.match) ~= 'string' then - throw_error('def.match has to be a string') + macro = function(value, key, def) + if value == nil then + return end - local match = string.match(value, def.match) - if match == nil then - throw_error( - 'The value “' .. value .. '” of the key “' .. key .. - '” does not match “' .. def.match .. '”!') - else - return match + if def.macro ~= nil then + token.set_macro(def.macro, value, 'global') end - end - end, + end, - opposite_keys = function(value, key, def) - if def.opposite_keys ~= nil then - local true_value = def.opposite_keys[true] - local false_value = def.opposite_keys[false] - if true_value == nil or false_value == nil then - throw_error( - 'Usage opposite_keys = { [true] = "...", [false] = "..." }') + match = function(value, key, def) + if value == nil then + return end - if utils.remove_from_array(input, true_value) ~= nil then - return true - elseif utils.remove_from_array(input, false_value) ~= nil then - return false + if def.match ~= nil then + if type(def.match) ~= 'string' then + throw_error('E008') + end + local match = string.match(value, def.match) + if match == nil then + throw_error('E009', { + value = value, + key = key, + match = def.match:gsub('%%', '%%%%'), + }) + else + return match + end end - end - end, + end, - process = function(value, key, def) - if value == nil then - return - end - if def.process ~= nil and type(def.process) == 'function' then - return def.process(value, input_root, output, unknown) - end - end, + opposite_keys = function(value, key, def) + if def.opposite_keys ~= nil then + local function get_value(key1, key2) + local opposite_name + if def.opposite_keys[key1] ~= nil then + opposite_name = def.opposite_keys[key1] + elseif def.opposite_keys[key2] ~= nil then + opposite_name = def.opposite_keys[key2] + end + return opposite_name + end + local true_key = get_value(true, 1) + local false_key = get_value(false, 2) + if true_key == nil or false_key == nil then + throw_error('E010') + end - pick = function(value, key, def) - if def.pick then - local pick_types + ---@param value string + local function remove_values(value) + local count = 0 + while utils.remove_from_table(input, value) do + count = count + 1 + end + return count + end - -- Allow old deprecated attribut pick = true - if def.pick == true then - pick_types = { 'any' } - elseif type(def.pick) == 'table' then - pick_types = def.pick - else - pick_types = { def.pick } - end + local true_count = remove_values(true_key) + local false_count = remove_values(false_key) + + if true_count > 1 then + throw_error('E021', { key = true_key }) + end + + if false_count > 1 then + throw_error('E021', { key = false_key }) + end - -- Check if the pick attribute is valid - for _, pick_type in ipairs(pick_types) do - if type(pick_type) == 'string' and is[pick_type] == nil then - throw_error( - 'Wrong data type in the “pick” attribute: ' .. - tostring(pick_type) .. - '. Allowed are: any, boolean, dimension, integer, number, string.') + if true_count > 0 and false_count > 0 then + throw_error('E020', + { ['true'] = true_key, ['false'] = false_key }) end + + return true_count == 1 or false_count == 0 end + end, - -- The key has already a value. We leave the function at this - -- point to be able to check the pick attribute for errors - -- beforehand. - if value ~= nil then - return value + process = function(value, key, def) + if value == nil then + return + end + if def.process ~= nil and type(def.process) == 'function' then + return def.process(value, input_root, output, unknown) end + end, - for _, pick_type in ipairs(pick_types) do - for i, v in pairs(input) do - -- We can not use ipairs here. `ipairs(t)` iterates up to the - -- first absent index. Values are deleted from the `input` - -- table. - if type(i) == 'number' then - local picked_value = nil - if is[pick_type](v) then - picked_value = v - end + pick = function(value, key, def) + if def.pick then + local pick_types - if picked_value ~= nil then - input[i] = nil - return picked_value - end + ---Allow old deprecated attribut pick = true + if def.pick == true then + pick_types = { 'any' } + elseif type(def.pick) == 'table' then + pick_types = def.pick + else + pick_types = { def.pick } + end + + ---Check if the pick attribute is valid + for _, pick_type in ipairs(pick_types) do + if type(pick_type) == 'string' and is[pick_type] == nil then + throw_error('E011', { + unknown = tostring(pick_type), + data_types = { + 'any', + 'boolean', + 'dimension', + 'integer', + 'number', + 'string', + }, + }) end end - end - end - end, - required = function(value, key, def) - if def.required ~= nil and def.required and value == nil then - throw_error(string.format('Missing required key “%s”!', key)) - end - end, + ---The key has already a value. We leave the function at this + ---point to be able to check the pick attribute for errors + ---beforehand. + if value ~= nil then + return value + end - sub_keys = function(value, key, def) - if def.sub_keys ~= nil then - local v - -- To get keys defined with always_present - if value == nil then - v = {} - elseif type(value) == 'string' then - v = { value } - elseif type(value) == 'table' then - v = value + for _, pick_type in ipairs(pick_types) do + for i, v in pairs(input) do + ---We can not use ipairs here. `ipairs(t)` iterates up to the + ---first absent index. Values are deleted from the `input` + ---table. + if type(i) == 'number' then + local picked_value = nil + if is[pick_type](v) then + picked_value = v + end + + if picked_value ~= nil then + input[i] = nil + return picked_value + end + end + end + end end - v = apply_definitions(def.sub_keys, opts, v, output[key], - unknown, add_to_key_path(key_path, key), input_root) - if utils.get_table_size(v) > 0 then - return v + end, + + required = function(value, key, def) + if def.required ~= nil and def.required and value == nil then + throw_error('E012', { key = key }) end - end - end, - } + end, - --- standalone values are removed. - -- For some callbacks and the third return value of parse, we - -- need an unchanged raw result from the parse function. - input = clone_table(input) - if output == nil then - output = {} - end - if unknown == nil then - unknown = {} - end - if key_path == nil then - key_path = {} - end + sub_keys = function(value, key, def) + if def.sub_keys ~= nil then + local v + ---To get keys defined with always_present + if value == nil then + v = {} + elseif type(value) == 'string' then + v = { value } + elseif type(value) == 'table' then + v = value + end + v = apply_definitions(def.sub_keys, opts, v, output[key], + unknown, add_to_key_path(key_path, key), input_root) + if utils.get_table_size(v) > 0 then + return v + end + end + end, + } - for index, def in pairs(defs) do - -- Find key and def - local key - -- `{ key1 = { }, key2 = { } }` - if type(def) == 'table' and def.name == nil and type(index) == - 'string' then - key = index - -- `{ { name = 'key1' }, { name = 'key2' } }` - elseif type(def) == 'table' and def.name ~= nil then - key = def.name - -- Definitions as strings in an array: `{ 'key1', 'key2' }` - elseif type(index) == 'number' and type(def) == 'string' then - key = def - def = { default = get_default_value({}) } + ---standalone values are removed. + ---For some callbacks and the third return value of parse, we + ---need an unchanged raw result from the parse function. + input = utils.clone_table(input) + if output == nil then + output = {} end - - if type(def) ~= 'table' then - throw_error('Key definition must be a table!') + if unknown == nil then + unknown = {} + end + if key_path == nil then + key_path = {} end - for attr, _ in pairs(def) do - if namespace.attrs[attr] == nil then - throw_error('Unknown definition attribute: ' .. tostring(attr)) + for index, def in pairs(defs) do + ---Find key and def + local key + ---`{ key1 = { }, key2 = { } }` + if type(def) == 'table' and def.name == nil and type(index) == + 'string' then + key = index + ---`{ { name = 'key1' }, { name = 'key2' } }` + elseif type(def) == 'table' and def.name ~= nil then + key = def.name + ---Definitions as strings in an array: `{ 'key1', 'key2' }` + elseif type(index) == 'number' and type(def) == 'string' then + key = def + def = { default = get_default_value({}) } end - end - if key == nil then - throw_error('Key name couldn’t be detected!') - end + if type(def) ~= 'table' then + throw_error('E013', { data_type = tostring(def), key = index }) ---key is nil + end - local value = find_value(key, def) - - for _, def_opt in ipairs({ - 'alias', - 'opposite_keys', - 'pick', - 'always_present', - 'required', - 'data_type', - 'choices', - 'match', - 'exclusive_group', - 'macro', - 'l3_tl_set', - 'process', - 'sub_keys', - }) do - if def[def_opt] ~= nil then - local tmp_value = apply[def_opt](value, key, def) - if tmp_value ~= nil then - value = tmp_value + for attr, _ in pairs(def) do + if namespace.attrs[attr] == nil then + throw_error('E014', { + unknown = attr, + attr_names = utils.get_table_keys(namespace.attrs), + }) end end - end - output[key] = value - end + if key == nil then + throw_error('E015') + end - if utils.get_table_size(input) > 0 then - -- Move to the current unknown table. - local current_unknown = unknown - for _, key in ipairs(key_path) do - if current_unknown[key] == nil then - current_unknown[key] = {} + local value = find_value(key, def) + + for _, def_opt in ipairs({ + 'alias', + 'opposite_keys', + 'pick', + 'always_present', + 'required', + 'data_type', + 'choices', + 'match', + 'exclusive_group', + 'macro', + 'l3_tl_set', + 'process', + 'sub_keys', + }) do + if def[def_opt] ~= nil then + local tmp_value = apply[def_opt](value, key, def) + if tmp_value ~= nil then + value = tmp_value + end + end end - current_unknown = current_unknown[key] + + output[key] = value end - -- Copy all unknown key-value-pairs to the current unknown table. - for key, value in pairs(input) do - current_unknown[key] = value + if utils.get_table_size(input) > 0 then + ---Move to the current unknown table. + local current_unknown = unknown + for _, key in ipairs(key_path) do + if current_unknown[key] == nil then + current_unknown[key] = {} + end + current_unknown = current_unknown[key] + end + + ---Copy all unknown key-value-pairs to the current unknown table. + for key, value in pairs(input) do + current_unknown[key] = value + end end + + return output, unknown end - return output, unknown -end + --- + ---Parse a LaTeX/TeX style key-value string into a Lua table. + --- + ---@param kv_string string # A string in the TeX/LaTeX style key-value format as described above. + ---@param opts? table # A table containing options. + --- + ---@return table result # The final result of all individual parsing and normalization steps. + ---@return table unknown # A table with unknown, undefinied key-value pairs. + ---@return table raw # The unprocessed, raw result of the LPeg parser. + local function parse(kv_string, opts) + if kv_string == nil then + return {}, {}, {} + end ---- Parse a LaTeX/TeX style key-value string into a Lua table. ---- ----@param kv_string string # A string in the TeX/LaTeX style key-value format as described above. ----@param opts? table # A table containing the settings: ---- `convert_dimensions`, `unpack`, `naked_as_value`, `converter`, ---- `debug`, `preprocess`, `postprocess`. --- ----@return table result # The final result of all individual parsing and normalization steps. ----@return table unknown # A table with unknown, undefinied key-value pairs. ----@return table raw # The unprocessed, raw result of the LPeg parser. -local function parse(kv_string, opts) - if kv_string == nil then - return {}, {}, {} - end + opts = normalize_opts(opts) - opts = normalize_opts(opts) + if type(opts.hooks.kv_string) == 'function' then + kv_string = opts.hooks.kv_string(kv_string) + end - if type(opts.hooks.kv_string) == 'function' then - kv_string = opts.hooks.kv_string(kv_string) - end + local result = generate_parser('list', opts):match(kv_string) + local raw = utils.clone_table(result) - local result = generate_parser('list', opts):match(kv_string) - local raw = clone_table(result) + local function apply_hook(name) + if type(opts.hooks[name]) == 'function' then + if name:match('^keys') then + result = utils.visit_tree(result, opts.hooks[name]) + else + opts.hooks[name](result) + end - local function apply_hook(name) - if type(opts.hooks[name]) == 'function' then - if name:match('^keys') then - result = visit_tree(result, opts.hooks[name]) - else - opts.hooks[name](result) + if opts.debug then + print('After the execution of the hook: ' .. name) + visualizers.debug(result) + end end + end - if opts.debug then - print('After the execution of the hook: ' .. name) - debug(result) + local function apply_hooks(at) + if at ~= nil then + at = '_' .. at + else + at = '' end + apply_hook('keys' .. at) + apply_hook('result' .. at) end - end - local function apply_hooks(at) - if at ~= nil then - at = '_' .. at - else - at = '' - end - apply_hook('keys' .. at) - apply_hook('result' .. at) - end + apply_hooks('before_opts') + + --- + ---Normalize the result table of the LPeg parser. This normalization + ---tasks are performed on the raw input table coming directly from + ---the PEG parser: + -- + ---@param result table # The raw input table coming directly from the PEG parser + ---@param opts table # Some options. + local function apply_opts(result, opts) + local callbacks = { + unpack = function(key, value) + if type(value) == 'table' and utils.get_array_size(value) == 1 and + utils.get_table_size(value) == 1 and type(value[1]) ~= + 'table' then + return key, value[1] + end + return key, value + end, - apply_hooks('before_opts') + process_naked = function(key, value) + if type(key) == 'number' and type(value) == 'string' then + return value, opts.default + end + return key, value + end, + + format_key = function(key, value) + if type(key) == 'string' then + for _, style in ipairs(opts.format_keys) do + if style == 'lower' then + key = key:lower() + elseif style == 'snake' then + key = key:gsub('[^%w]+', '_') + elseif style == 'upper' then + key = key:upper() + else + throw_error('E017', { + unknown = style, + styles = { 'lower', 'snake', 'upper' }, + }) + end + end + end + return key, value + end, - --- Normalize the result table of the LPeg parser. This normalization - -- tasks are performed on the raw input table coming directly from - -- the PEG parser: - -- - ---@param result table # The raw input table coming directly from the PEG parser - ---@param opts table # Some options. - local function apply_opts(result, opts) - local callbacks = { - unpack = function(key, value) - if type(value) == 'table' and utils.get_array_size(value) == 1 and - utils.get_table_size(value) == 1 and type(value[1]) ~= 'table' then - return key, value[1] - end - return key, value - end, + apply_invert_flag = function(key, value) + if type(key) == 'string' and key:find(opts.invert_flag) then + return key:gsub(opts.invert_flag, ''), not value + end + return key, value + end, + } - process_naked = function(key, value) - if type(key) == 'number' and type(value) == 'string' then - return value, opts.default - end - return key, value - end, + if opts.unpack then + result = utils.visit_tree(result, callbacks.unpack) + end - format_key = function(key, value) - if type(key) == 'string' then - for _, style in ipairs(opts.format_keys) do - if style == 'lower' then - key = key:lower() - elseif style == 'snake' then - key = key:gsub('[^%w]+', '_') - elseif style == 'upper' then - key = key:upper() - else - throw_error('Unknown style to format keys: ' .. - tostring(style) .. - ' Allowed styles are: lower, snake, upper') - end - end - end - return key, value - end, + if not opts.naked_as_value and opts.defs == false then + result = utils.visit_tree(result, callbacks.process_naked) + end - apply_invert_flag = function(key, value) - if type(key) == 'string' and key:find(opts.invert_flag) then - return key:gsub(opts.invert_flag, ''), not value + if opts.format_keys then + if type(opts.format_keys) ~= 'table' then + throw_error('E018', { data_type = type(opts.format_keys) }) end - return key, value - end, - } + result = utils.visit_tree(result, callbacks.format_key) + end - if opts.unpack then - result = visit_tree(result, callbacks.unpack) + if opts.invert_flag then + result = utils.visit_tree(result, callbacks.apply_invert_flag) + end + + return result end + result = apply_opts(result, opts) + + ---All unknown keys are stored in this table + local unknown = nil + if type(opts.defs) == 'table' then + apply_hooks('before_defs') + result, unknown = apply_definitions(opts.defs, opts, result, {}, + {}, {}, utils.clone_table(result)) + end + + apply_hooks() - if not opts.naked_as_value and opts.defs == false then - result = visit_tree(result, callbacks.process_naked) + if opts.defaults ~= nil and type(opts.defaults) == 'table' then + utils.merge_tables(result, opts.defaults, false) end - if opts.format_keys then - if type(opts.format_keys) ~= 'table' then - throw_error( - 'The option “format_keys” has to be a table not ' .. - type(opts.format_keys)) - end - result = visit_tree(result, callbacks.format_key) + if opts.debug then + visualizers.debug(result) end - if opts.invert_flag then - result = visit_tree(result, callbacks.apply_invert_flag) + if opts.accumulated_result ~= nil and type(opts.accumulated_result) == + 'table' then + utils.merge_tables(opts.accumulated_result, result, true) end - return result - end - result = apply_opts(result, opts) - - -- All unknown keys are stored in this table - local unknown = nil - if type(opts.defs) == 'table' then - apply_hooks('before_defs') - result, unknown = apply_definitions(opts.defs, opts, result, {}, {}, - {}, clone_table(result)) + ---no_error + if not opts.no_error and type(unknown) == 'table' and + utils.get_table_size(unknown) > 0 then + throw_error('E019', { unknown = visualizers.render(unknown) }) + end + return result, unknown, raw end - apply_hooks() - - if opts.defaults ~= nil and type(opts.defaults) == 'table' then - merge_tables(result, opts.defaults) - end + --- + ---A table to store parsed key-value results. + local result_store = {} - if opts.debug then - debug(result) - end + return { + new = main, - -- no_error - if not opts.no_error and type(unknown) == 'table' and - utils.get_table_size(unknown) > 0 then - throw_error('Unknown keys: ' .. render(unknown)) - end - return result, unknown, raw -end + version = { 0, 12, 0 }, ---- Store results --- @section + ---@see parse + parse = parse, ---- A table to store parsed key-value results. -local result_store = {} + define = function(defs, opts) + return function(kv_string, inner_opts) + local options ---- Exports --- @section + if inner_opts ~= nil and opts ~= nil then + options = utils.merge_tables(opts, inner_opts) + elseif inner_opts ~= nil then + options = inner_opts + elseif opts ~= nil then + options = opts + end -local export = { - version = { 0, 11, 0 }, + if options == nil then + options = {} + end - ---Get a new instance of the luakeys module. - --- - ---@return table # A new instance of the luakeys module. - get_private_instance = function() - package.loaded.luakeys = nil - local luakeys = require('luakeys') - package.loaded.luakeys = nil - return luakeys - end, + options.defs = defs - namespace = namespace, + return parse(kv_string, options) + end + end, - ---This function is used in the documentation. - --- - ---@param from string # A key in the namespace table, either `opts`, `hook` or `attrs`. - print_names = function(from) - local names = {} - for name in pairs(namespace[from]) do - table.insert(names, name) - end - table.sort(names) - tex.print(table.concat(names, ', ')) - end, + ---@see default_opts + opts = default_opts, - print_default = function(from, name) - tex.print(tostring(namespace[from][name])) - end, + error_messages = error_messages, - --- @see default_options - opts = default_options, + ---@see visualizers.render + render = visualizers.render, - --- @see stringify - stringify = stringify, + ---@see visualizers.stringify + stringify = visualizers.stringify, - --- @see parse - parse = parse, + ---@see visualizers.debug + debug = visualizers.debug, - define = function(defs, opts) - return function(kv_string, inner_opts) - local options + --- + ---The function `save(identifier, result): void` saves a result (a + ---table from a previous run of `parse`) under an identifier. + ---Therefore, it is not necessary to pollute the global namespace to + ---store results for the later usage. + --- + ---@param identifier string # The identifier under which the result is saved. + --- + ---@param result table # A result to be stored and that was created by the key-value parser. + save = function(identifier, result) + result_store[identifier] = result + end, - if inner_opts ~= nil and opts ~= nil then - options = merge_tables(opts, inner_opts) - elseif inner_opts ~= nil then - options = inner_opts - elseif opts ~= nil then - options = opts - end + ---The function `get(identifier): table` retrieves a saved result + ---from the result store. + --- + ---@param identifier string # The identifier under which the result was saved. + --- + ---@return table + get = function(identifier) + ---if result_store[identifier] == nil then + --- throw_error('No stored result was found for the identifier \'' .. identifier .. '\'') + ---end + return result_store[identifier] + end, - if options == nil then - options = {} - end + is = is, - options.defs = defs + utils = utils, - return parse(kv_string, options) - end - end, + --- + ---Exported but intentionally undocumented functions + --- - --- @see render - render = render, + namespace = utils.clone_table(namespace), - --- @see debug - debug = debug, + --- + ---This function is used in the documentation. + --- + ---@param from string # A key in the namespace table, either `opts`, `hook` or `attrs`. + print_names = function(from) + local names = utils.get_table_keys(namespace[from]) + tex.print(table.concat(names, ', ')) + end, - --- The function `save(identifier, result): void` saves a result (a - -- table from a previous run of `parse`) under an identifier. - -- Therefore, it is not necessary to pollute the global namespace to - -- store results for the later usage. - -- - ---@param identifier string # The identifier under which the result is saved. - -- - ---@param result table # A result to be stored and that was created by the key-value parser. - save = function(identifier, result) - result_store[identifier] = result - end, + print_default = function(from, name) + tex.print(tostring(namespace[from][name])) + end, - --- The function `get(identifier): table` retrieves a saved result - -- from the result store. - -- - ---@param identifier string # The identifier under which the result was saved. - --- - ---@return table - get = function(identifier) - -- if result_store[identifier] == nil then - -- throw_error('No stored result was found for the identifier \'' .. identifier .. '\'') - -- end - return result_store[identifier] - end, + --- + ---@param exported_table table + depublish_functions = function(exported_table) + local function warn_global_import() + throw_error('E023') + end - is = is, + exported_table.parse = warn_global_import + exported_table.define = warn_global_import + exported_table.save = warn_global_import + exported_table.get = warn_global_import + end, + } - utils = utils, -} +end -return export +return main diff --git a/Master/texmf-dist/tex/luatex/luakeys/luakeys.sty b/Master/texmf-dist/tex/luatex/luakeys/luakeys.sty index 8cb59cf5ef5..4b5432faa3b 100644 --- a/Master/texmf-dist/tex/luatex/luakeys/luakeys.sty +++ b/Master/texmf-dist/tex/luatex/luakeys/luakeys.sty @@ -1,5 +1,5 @@ %% luakeys.sty -%% Copyright 2021-2022 Josef Friedrich +%% Copyright 2021-2023 Josef Friedrich % % This work may be distributed and/or modified under the % conditions of the LaTeX Project Public License, either version 1.3c @@ -17,5 +17,14 @@ % luakeys-debug.sty and luakeys-debug.tex. \NeedsTeXFormat{LaTeX2e} -\ProvidesPackage{luakeys}[2022/12/23 0.11.0 Parsing key-value options using Lua.] -\directlua{luakeys = require('luakeys')} +\ProvidesPackage{luakeys}[2023/01/05 v0.12.0 Parsing key-value options using Lua.] +\directlua{ + if luakeys == nil then + luakeys = require('luakeys')() + luakeys.depublish_functions(luakeys) + end +} + +\def\LuakeysGetPackageOptions{\luaescapestring{\@ptionlist{\@currname.\@currext}}} + +\def\LuakeysGetClassOptions{\luaescapestring{\@raw@classoptionslist}} diff --git a/Master/texmf-dist/tex/luatex/luakeys/luakeys.tex b/Master/texmf-dist/tex/luatex/luakeys/luakeys.tex index c3c69a0ba35..f993e7c83fb 100644 --- a/Master/texmf-dist/tex/luatex/luakeys/luakeys.tex +++ b/Master/texmf-dist/tex/luatex/luakeys/luakeys.tex @@ -1,5 +1,5 @@ %% luakeys.tex -%% Copyright 2021-2022 Josef Friedrich +%% Copyright 2021-2023 Josef Friedrich % % This work may be distributed and/or modified under the % conditions of the LaTeX Project Public License, either version 1.3c @@ -16,4 +16,9 @@ % This work consists of the files luakeys.lua, luakeys.sty, luakeys.tex % luakeys-debug.sty and luakeys-debug.tex. -\directlua{luakeys = require('luakeys')} +\directlua{ + if luakeys == nil then + luakeys = require('luakeys')() + luakeys.depublish_functions(luakeys) + end +} -- cgit v1.2.3