summaryrefslogtreecommitdiff
path: root/Master/texmf-dist/tex/lualatex
diff options
context:
space:
mode:
authorKarl Berry <karl@freefriends.org>2022-10-30 20:46:45 +0000
committerKarl Berry <karl@freefriends.org>2022-10-30 20:46:45 +0000
commit00f34d83a0e5a739dda789aebdb2871f3cbeddd8 (patch)
tree46006acae9ad4313051c0cb504b371e95ba8a40a /Master/texmf-dist/tex/lualatex
parent418009d3a363c5b15f39b7a52c22aa0ba84b6a66 (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')
-rw-r--r--Master/texmf-dist/tex/lualatex/luaoptions/luaoptions-lib.lua240
-rw-r--r--Master/texmf-dist/tex/lualatex/luaoptions/luaoptions.lua329
-rw-r--r--Master/texmf-dist/tex/lualatex/luaoptions/luaoptions.sty36
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}')
+ }%
+}