diff options
author | Karl Berry <karl@freefriends.org> | 2022-10-30 20:46:45 +0000 |
---|---|---|
committer | Karl Berry <karl@freefriends.org> | 2022-10-30 20:46:45 +0000 |
commit | 00f34d83a0e5a739dda789aebdb2871f3cbeddd8 (patch) | |
tree | 46006acae9ad4313051c0cb504b371e95ba8a40a /Master/texmf-dist/tex/lualatex | |
parent | 418009d3a363c5b15f39b7a52c22aa0ba84b6a66 (diff) |
luaoptions (30oct22)
git-svn-id: svn://tug.org/texlive/trunk@64870 c570f23f-e606-0410-a88d-b1316a301751
Diffstat (limited to 'Master/texmf-dist/tex/lualatex')
3 files changed, 605 insertions, 0 deletions
diff --git a/Master/texmf-dist/tex/lualatex/luaoptions/luaoptions-lib.lua b/Master/texmf-dist/tex/lualatex/luaoptions/luaoptions-lib.lua new file mode 100644 index 00000000000..4f797da12e9 --- /dev/null +++ b/Master/texmf-dist/tex/lualatex/luaoptions/luaoptions-lib.lua @@ -0,0 +1,240 @@ +-- luacheck: ignore ly log self luatexbase internalversion font fonts tex token kpse status +local err, warn, info, log = luatexbase.provides_module({ + name = "luaoptions-lib", + version = '0.8', --LUAOPTIONS_VERSION + date = "2022/10/30", --LUAOPTIONS_DATE + description = "Module luaoptions-lib.", + author = "The lualatex-tools Project", + copyright = "2015-2022 - the lualatex-tools Project", + license = "MIT", +}) + +local lib = {} +lib.TEX_UNITS = {'bp', 'cc', 'cm', 'dd', 'in', 'mm', 'pc', 'pt', 'sp', 'em', +'ex'} + +------------------------- +-- General tool functions + +function lib.basename(str) +--[[ + Given the full path to a file, return only the file name (without path). + If there is no slash, return the full name. +--]] + return str:gsub(".*/(.*)", "%1") or str +end + + +function lib.contains(table_var, value) +--[[ + Returns the key if the given table contains the given value, or nil. + A value of 'false' (string) is considered equal to false (Boolean). +--]] + for k, v in pairs(table_var) do + if v == value then return k + elseif v == 'false' and value == false then return k + end + end +end + + +function lib.contains_key(table_var, key) +-- Returs true if the given key is present in the table, nil otherwise. + for k in pairs(table_var) do + if k == key then return true end + end +end + + +function lib.convert_unit(value) +--[[ + Convert a LaTeX unit, if possible. + TODO: Understand what this *really* does, what is accepted and returned. +--]] + if not value then return 0 + elseif value == '' then return false + elseif value:match('\\') then + local n, u = value:match('^%d*%.?%d*'), value:match('%a+') + if n == '' then n = 1 end + return tonumber(n) * tex.dimen[u] / tex.sp("1pt") + else return ('%f'):format(tonumber(value) or tex.sp(value) / tex.sp("1pt")) + end +end + + +function lib.current_font_size() +--[[ + Convenience function to return the font size of the current font +--]] + return lib.fontinfo(font.current()).size +end + + +function lib.dirname(str) +--[[ + Given the full path to a file, return only the path (without file name), + including the last slash. If there is no slash, return an empty string. +--]] + return str:gsub("(.*/).*", "%1") or '' +end + + +local fontdata = fonts.hashes.identifiers +function lib.fontinfo(id) +--[[ + Return a LuaTeX font object based on the given ID +--]] + return fontdata[id] or font.getfont(id) +end + + +function lib.max(a, b) + a, b = tonumber(a), tonumber(b) + if a > b then return a else return b end +end + + +function lib.min(a, b) + a, b = tonumber(a), tonumber(b) + if a < b then return a else return b end +end + + +function lib.mkdirs(str) + local path + if str:sub(1, 1) == '/' then path = '' else path = '.' end + for dir in str:gmatch('([^%/]+)') do + path = path .. '/' .. dir + lfs.mkdir(path) + end +end + + +function lib.orderedkeys(t) + local orderedIndex = {} + for k in pairs(t) do table.insert(orderedIndex, k) end + table.sort(orderedIndex) + return orderedIndex +end + + +function lib.orderedpairs(t) + local key + local i = 0 + local orderedIndex = lib.orderedkeys(t) + return function () + i = i + 1 + key = orderedIndex[i] + if key then return key, t[key] end + end +end + + +function lib.print_table(t, indent) +-- Recursively print a table, avoiding recursion loops + indent = indent or ' ' + local visited = {} + local skips = 0 + + local function _print_table(_t, _ind) + local keys = lib.orderedkeys(_t) + local display_keys = {} + local length = 0 + for key, _ in pairs(_t) do + length = lib.max(length, #tostring(key)) + end + for key, _ in pairs(_t) do + display_key = tostring(key) + display_keys[key] = display_key .. string.rep( + ' ', length - #display_key) + end + for k, v in lib.orderedpairs(_t) do + if type(v) == 'table' then + if visited[v] then + skips = skips + 1 + else + visited[v] = true + print(_ind..display_keys[k], v) + if #_ind > 40 then err("k") end + _print_table(v, _ind..indent) + end + else + if v == '' then v = '(empty string)' end + print(_ind..display_keys[k], v) + end + end + end + + print() + visited[t] = true + print("Print table:", t) + _print_table(t, indent) + print() + if skips > 0 then + print(string.format('%s recursive tables skipped', skips)) + end + print("============") + print() +end + +function lib.readlinematching(s, f) + if f then + local result = '' + while result and not result:find(s) do result = f:read() end + f:close() + return result + end +end + + +function lib.splitext(str, ext) +--[[ + If 'ext' is supplied return str stripped of the given extension, + otherwise return the base and extension (if any) +--]] + return ext and (str:match('(.*)%.'..ext..'$') or str) + or (str:match('(.*)%.(%w*)$') or str) +end + + +------------------------------------ +-- Engine, version, TeX distribution + +local function _loaded(ext) +--[[ + Returns a function that checks whether a class or a package is loaded + https://tex.stackexchange.com/questions/501635/ +--]] + local fmt = "ver@%s." .. ext + return function(name) + local macro = token.get_macro(fmt:format(name)) + return macro ~= nil + end +end + +-- Lua implementations of \@ifpackageloaded and \@ifclassloaded +lib.class_loaded = _loaded('cls') +lib.package_loaded = _loaded('sty') + +local tex_engine = {} +setmetatable(tex_engine, tex_engine) + +function tex_engine:__call() +--[[ + Defines the properties extracted from the first line of jobname.log. +--]] + local f = io.open(tex.jobname..'.log') + if not f then return end + self.engine, self.engine_version, self.dist, self.dist_version, self.format, self.format_version = + f:read():match( + 'This is ([^,]*), Version ([^%(]*) %(([^%)]*) ([^%)]*)%)%s+%(format=([^%)]*) ([^)]*)%)' + ) + f:close() + return self +end + +function tex_engine:__index(k) return rawget(self(), k) end + + +lib.tex_engine = tex_engine +return lib diff --git a/Master/texmf-dist/tex/lualatex/luaoptions/luaoptions.lua b/Master/texmf-dist/tex/lualatex/luaoptions/luaoptions.lua new file mode 100644 index 00000000000..16f25150bfb --- /dev/null +++ b/Master/texmf-dist/tex/lualatex/luaoptions/luaoptions.lua @@ -0,0 +1,329 @@ +-- luacheck: ignore ly warn info log self luatexbase internalversion font fonts tex token kpse status +local err, warn, info, log = luatexbase.provides_module({ + name = "luaoptions", + version = '0.8', --LUAOPTIONS_VERSION + date = "2022/10/30", --LUAOPTIONS_DATE + description = "Module luaoptions.", + author = "The lualatex-tools Project", + copyright = "2015-2022 - the lualatex-tools Project", + 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("luaoptions-lib.lua") or "luaoptions-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) +local clients = {} + +function Opts:new(declarations) +--[[ + Declare an options object along with default and accepted values. + To *some* extent also provide type checking. + - 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 + ) + for k, v in pairs(declarations) do + o.options[k] = v[1] or '' + end + 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:has_option(name) +--[[ + Returns true if the given option is declared +--]] + return self.declarations[name] ~= nil +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:use_option(key) +--[[ + Call an option and write its value to the TeX space. + This is a shorthand for accessing options from the TeX side + (rather than having to write tex.print(XX_opts.some_options)) +--]] + local option = self.options[key] + if option then + tex.print(option:explode('\n')) + else + err(string.format("Trying to access non-existent option %s", key)) + 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 + +function optlib.register(client_name, declarations) +--[[ + Register a client as package options. + - Create a new Opts object, + - initialize it, + - store it in a global variable <client_name>_opts, + - write the code to handle the package options. +--]] + local o = Opts:new(declarations) + local exopt = '' + for k, v in pairs(declarations) do + tex.sprint(string.format([[ +\DeclareOptionX{%s}{\setluaoption{%s}{%s}{#1}}%% +]], + k, client_name, k + )) + exopt = exopt..k..'='..(v[1] or '')..',' + end + clients[client_name] = o + tex.sprint([[\ExecuteOptionsX{]]..exopt..[[}%%]], [[\ProcessOptionsX]]) +end + +function optlib.client(name) +--[[ + Return the FormattersTable instance registered with the given client name. + Return 'nil' if no client is found. +--]] + return clients[name] +end + +function optlib.get_option(client_name, k) +--[[ + Get an option's value from a registered client. + Raises an error if the client hasn't been registered. +--]] + local client = optlib.client(client_name) + return client.options[k] +end + +function optlib.set_option(client_name, k, v) +--[[ + Set an option. + Look up a registered client and set an option. + Produces an error if the client hasn't been registered. +--]] + local client = optlib.client(client_name) + client:set_option(k, v) +end + +function optlib.use_option(client_name, k) +--[[ + Look up an option and write it to LaTex. +--]] + local client = optlib.client(client_name) + client:use_option(k) +end + +optlib.Opts = Opts +return optlib diff --git a/Master/texmf-dist/tex/lualatex/luaoptions/luaoptions.sty b/Master/texmf-dist/tex/lualatex/luaoptions/luaoptions.sty new file mode 100644 index 00000000000..559c615ab69 --- /dev/null +++ b/Master/texmf-dist/tex/lualatex/luaoptions/luaoptions.sty @@ -0,0 +1,36 @@ +% luaoptions support package. +% +% Copyright (C) 2015-2022 the lualatex-tools Project +% License: MIT +% This file is part of luaoptions. + +\NeedsTeXFormat{LaTeX2e}% +\ProvidesPackage{luaoptions}[2022/10/30 v0.8] + +% Dependencies +\RequirePackage{luatexbase} +\RequirePackage{luaotfload} +\RequirePackage{xkeyval} + +\directlua{ + lua_options = require(kpse.find_file("luaoptions.lua") or "luaoptions.lua") +} + +% Set an option in a registered client +\newcommand{\setluaoption}[3]{% + \directlua{ + lua_options.set_option( + '\luatexluaescapestring{#1}', + '\luatexluaescapestring{#2}', + '\luatexluaescapestring{#3}') + } +} + +% Use an option and directly write it to LaTeX +\newcommand{\useluaoption}[2]{% + \directlua{ + lua_options.use_option( + '\luatexluaescapestring{#1}', + '\luatexluaescapestring{#2}') + }% +} |