diff options
Diffstat (limited to 'Master/texmf-dist/scripts/lyluatex/lyluatex-options.lua')
-rwxr-xr-x | Master/texmf-dist/scripts/lyluatex/lyluatex-options.lua | 266 |
1 files changed, 266 insertions, 0 deletions
diff --git a/Master/texmf-dist/scripts/lyluatex/lyluatex-options.lua b/Master/texmf-dist/scripts/lyluatex/lyluatex-options.lua new file mode 100755 index 00000000000..62ec72a3c3c --- /dev/null +++ b/Master/texmf-dist/scripts/lyluatex/lyluatex-options.lua @@ -0,0 +1,266 @@ +-- luacheck: ignore ly warn info log self luatexbase internalversion font fonts tex token kpse status +local err, warn, info, log = luatexbase.provides_module({ + name = "lyluatex-options", + version = '1.0f', --LYLUATEX_VERSION + date = "2019/05/27", --LYLUATEX_DATE + description = "Module lyluatex-options.", + author = "The Gregorio Project − (see Contributors.md)", + copyright = "2015-2019 - jperon and others", + license = "MIT", +}) + +--[[ + This module provides functionality to handle package options and make them + configurable in a fine-grained fashion as + - package options + - local options (for individual instances of commands/environments) + - changed “from here on” within a document. + +-- ]] + +local lib = require(kpse.find_file("lyluatex-lib.lua") or "lyluatex-lib.lua") +local optlib = {} -- namespace for the returned table +local Opts = {options = {}} -- Options class +Opts.__index = function (self, k) return self.options[k] or rawget(Opts, k) end +setmetatable(Opts, Opts) + + +function Opts:new(opt_prefix, declarations) +--[[ + Declare package options along with their default and + accepted values. To *some* extent also provide type checking. + - opt_prefix: the prefix/name by which the lyluatex-options module is + referenced in the parent LaTeX document (preamble or package). + This is required to write the code calling optlib.set_option into + the option declaration. + - declarations: a definition table stored in the calling module (see below) + Each entry in the 'declarations' table represents one package option, with each + value being an array (table with integer indexes instead of keys). For + details please refer to the manual. +--]] + local o = setmetatable( + { + declarations = declarations, + options = {} + }, + self + ) + local exopt = '' + for k, v in pairs(declarations) do + o.options[k] = v[1] or '' + tex.sprint(string.format([[ +\DeclareOptionX{%s}{\directlua{ + %s:set_option('%s', '\luatexluaescapestring{#1}') +}}%% +]], + k, opt_prefix, k + )) + exopt = exopt..k..'='..(v[1] or '')..',' + end + tex.sprint([[\ExecuteOptionsX{]]..exopt..[[}%%]], [[\ProcessOptionsX]]) + return o +end + +function Opts:check_local_options(opts, ignore_declarations) +--[[ + Parse the given options (options passed to a command/environment), + sanitize them against the global package options and return a table + with the local options that should then supersede the global options. + If ignore_declaration is given any non-false value the sanitization + step is skipped (i.e. local options are only parsed and duplicates + rejected). +--]] + local options = {} + local next_opt = opts:gmatch('([^,]+)') -- iterator over options + for opt in next_opt do + local k, v = opt:match('([^=]+)=?(.*)') + if k then + if v and v:sub(1, 1) == '{' then -- handle keys with {multiple, values} + while select(2, v:gsub('{', '')) ~= select(2, v:gsub('}', '')) do v = v..','..next_opt() end + v = v:sub(2, -2) -- remove { } + end + if not ignore_declarations then + k, v = self:sanitize_option(k:gsub('^%s', ''), v:gsub('^%s', '')) + end + if k then + if options[k] then err('Option %s is set two times for the same score.', k) + else options[k] = v + end + end + end + end + return options +end + +function Opts:is_neg(k) +--[[ + Type check for a 'negative' option. This is an existing option + name prefixed with 'no' (e.g. 'noalign') +--]] + local _, i = k:find('^no') + return i and lib.contains_key(self.declarations, k:sub(i + 1)) +end + +function Opts:sanitize_option(k, v) +--[[ + Check and (if necessary) adjust the value of a given option. + Reject undefined options + Check 'negative' options + Handle boolean options (empty strings or 'false'), set them to real booleans +--]] + local declarations = self.declarations + if k == '' or k == 'noarg' then return end + if not lib.contains_key(declarations, k) then err('Unknown option: '..k) end + -- aliases + if declarations[k] and declarations[k][2] == optlib.is_alias then + if declarations[k][1] == v then return + else k = declarations[k] end + end + -- boolean + if v == 'false' then v = false end + -- negation (for example, noindent is the negation of indent) + if self:is_neg(k) then + if v ~= nil and v ~= 'default' then + k = k:gsub('^no(.*)', '%1') + v = not v + else return + end + end + return k, v +end + +function Opts:set_option(k, v) +--[[ + Set an option for the given prefix to be in effect from this point on. + Raises an error if the option is not declared or does not meet the + declared expectations. (TODO: The latter has to be integrated by extracting + optlib.validate_option from optlib.validate_options and call it in + sanitize_option). +--]] + k, v = self:sanitize_option(k, v) + if k then + self.options[k] = v + self:validate_option(k) + end +end + +function Opts:validate_option(key, options_obj) +--[[ + Validate an (already sanitized) option against its expected values. + With options_obj a local options table can be provided, + otherwise the global options stored in OPTIONS are checked. +--]] + local package_opts = self.declarations + local options = options_obj or self.options + local unexpected + if options[key] == 'default' then + -- Replace 'default' with an actual value + options[key] = package_opts[key][1] + unexpected = options[key] == nil + end + if not lib.contains(package_opts[key], options[key]) and package_opts[key][2] then + -- option value is not in the array of accepted values + if type(package_opts[key][2]) == 'function' then package_opts[key][2](key, options[key]) + else unexpected = true + end + end + if unexpected then + err([[ + Unexpected value "%s" for option %s: + authorized values are "%s" + ]], + options[key], key, table.concat(package_opts[key], ', ') + ) + end +end + +function Opts:validate_options(options_obj) +--[[ + Validate the given set of options against the option declaration + table for the given prefix. + With options_obj a local options table can be provided, + otherwise the global options stored in OPTIONS are checked. +--]] + for k, _ in lib.orderedpairs(self.declarations) do + self:validate_option(k, options_obj) + end +end + + +function optlib.is_alias() +--[[ + This function doesn't do anything, but if an option is defined + as an alias, its second parameter will be this function, so the + test declarations[k][2] == optlib.is_alias in Opts:sanitize_options + will return true. +--]] +end + + +function optlib.is_dim(k, v) +--[[ + Type checking for options that accept a LaTeX dimension. + This can be + - a number (integer or float) + - a number with unit + - a (multiplied) TeX length + (see error message in code for examples) +--]] + if v == '' or v == false or tonumber(v) then return true end + local n, sl, u = v:match('^%d*%.?%d*'), v:match('\\'), v:match('%a+') + -- a value of number - backslash - length is a dimension + -- invalid input will be prevented in by the LaTeX parser already + if n and sl and u then return true end + if n and lib.contains(lib.TEX_UNITS, u) then return true end + err([[ +Unexpected value "%s" for dimension %s: +should be either a number (for example "12"), +a number with unit, without space ("12pt"), +or a (multiplied) TeX length (".8\linewidth") +]], + v, k + ) +end + + +function optlib.is_neg(k, _) +--[[ + Type check for a 'negative' option. At this stage, + we only check that it begins with 'no'. +--]] + return k:find('^no') +end + + +function optlib.is_num(_, v) +--[[ + Type check for number options +--]] + return v == '' or tonumber(v) +end + + +function optlib.is_str(_, v) +--[[ + Type check for string options +--]] + return type(v) == 'string' +end + + +function optlib.merge_options(base_opt, super_opt) +--[[ + Merge two tables. + Create a new table as a copy of base_opt, then merge with + super_opt. Entries in super_opt supersede (i.e. overwrite) + colliding entries in base_opt. +--]] + local result = {} + for k, v in pairs(base_opt) do result[k] = v end + for k, v in pairs(super_opt) do result[k] = v end + return result +end + + +optlib.Opts = Opts +return optlib |