% \iffalse meta-comment % %% File: l3luatex.dtx % % Copyright (C) 2010-2024 The LaTeX Project % % It may be distributed and/or modified under the conditions of the % LaTeX Project Public License (LPPL), either version 1.3c of this % license or (at your option) any later version. The latest version % of this license is in the file % % https://www.latex-project.org/lppl.txt % % This file is part of the "l3kernel bundle" (The Work in LPPL) % and all files in that bundle must be distributed together. % % ----------------------------------------------------------------------- % % The development version of the bundle can be found at % % https://github.com/latex3/latex3 % % for those people who are interested. % %<*driver> \documentclass[full,kernel]{l3doc} \begin{document} \DocInput{\jobname.dtx} \end{document} % % \fi % % \title{^^A % The \pkg{l3luatex} module\\ \LuaTeX-specific functions^^A % } % % \author{^^A % The \LaTeX{} Project\thanks % {^^A % E-mail: % \href{mailto:latex-team@latex-project.org} % {latex-team@latex-project.org}^^A % }^^A % } % % \date{Released 2024-03-14} % % \maketitle % % \begin{documentation} % % The \LuaTeX{} engine provides access to the \Lua{} programming language, % and with it access to the \enquote{internals} of \TeX{}. In order to use % this within the framework provided here, a family of functions is % available. When used with \pdfTeX{}, \pTeX{}, \upTeX{} or \XeTeX{} these raise an % error: use \cs{sys_if_engine_luatex:T} to avoid this. Details on using % \Lua{} with the \LuaTeX{} engine are given in the \LuaTeX{} manual. % % \section{Breaking out to \Lua{}} % % \begin{function}[EXP, added = 2018-06-18]{\lua_now:n, \lua_now:e} % \begin{syntax} % \cs{lua_now:n} \Arg{token list} % \end{syntax} % The \meta{token list} is first tokenized by \TeX{}, which includes % converting line ends to spaces in the usual \TeX{} manner and which % respects currently-applicable \TeX{} category codes. The resulting % \meta{\Lua{} input} is passed to the \Lua{} interpreter for processing. % Each \cs{lua_now:n} block is treated by \Lua{} as a separate chunk. % The \Lua{} interpreter executes the \meta{\Lua{} input} immediately, % and in an expandable manner. % \begin{texnote} % \cs{lua_now:e} is a macro wrapper around \tn{directlua}: when % \LuaTeX{} is in use two expansions are required to yield the % result of the \Lua{} code. % \end{texnote} % \end{function} % % \begin{function}[added = 2018-06-18]{\lua_shipout_e:n, \lua_shipout:n} % \begin{syntax} % \cs{lua_shipout:n} \Arg{token list} % \end{syntax} % The \meta{token list} is first tokenized by \TeX{}, which includes % converting line ends to spaces in the usual \TeX{} manner and which % respects currently-applicable \TeX{} category codes. The resulting % \meta{\Lua{} input} is passed to the \Lua{} interpreter when the % current page is finalised (\emph{i.e.}~at shipout). Each % \cs{lua_shipout:n} block is treated by \Lua{} as a separate chunk. % The \Lua{} interpreter will execute the \meta{\Lua{} input} during the % page-building routine: no \TeX{} expansion of the \meta{\Lua{} input} % will occur at this stage. % % In the case of the \cs{lua_shipout_e:n} version the input is fully % expanded by \TeX{} in an \texttt{e}-type manner during the shipout % operation. % \begin{texnote} % At a \TeX{} level, the \meta{\Lua{} input} is stored as a % \enquote{whatsit}. % \end{texnote} % \end{function} % % \begin{function}[EXP, added = 2015-06-29]{\lua_escape:n, \lua_escape:e} % \begin{syntax} % \cs{lua_escape:n} \Arg{token list} % \end{syntax} % Converts the \meta{token list} such that it can safely be passed to % \Lua{}: embedded backslashes, double and single quotes, and newlines % and carriage returns are escaped. This is done by prepending an extra % token consisting of a backslash with category code~$12$, and for the line % endings, converting them to |\n| and |\r|, respectively. % \begin{texnote} % \cs{lua_escape:e} is a macro wrapper around \tn{luaescapestring}: % when \LuaTeX{} is in use two expansions are required to yield the % result of the \Lua{} code. % \end{texnote} % \end{function} % % \begin{function}[added = 2022-05-14]{\lua_load_module:n} % \begin{syntax} % \cs{lua_load_module:n} \Arg{Lua module name} % \end{syntax} % Loads a Lua module into the Lua interpreter. % % \cs{lua_now:n} passes its \Arg{token list} argument to the Lua interpreter % as a single line, with characters interpreted under the current catcode % regime. These two facts mean that \cs{lua_now:n} rarely behaves as expected % for larger pieces of code. Therefore, package authors should \textbf{not} % write significant amounts of Lua code in the arguments to \cs{lua_now:n}. % Instead, it is strongly recommended that they write the majorty of their Lua % code in a separate file, and then load it using \cs{lua_load_module:n}. % \begin{texnote} % This is a wrapper around the Lua call |require '|\meta{module}|'|. % \end{texnote} % \end{function} % % \section{Lua interfaces} % % As well as interfaces for \TeX{}, there are a small number of Lua functions % provided here. % % \begin{function}{ltx.utils} % Most public interfaces provided by the module are stored within the % |ltx.utils| table. % \end{function} % % \begin{function}{ltx.utils.filedump} % \begin{syntax} % \meta{dump}| = ltx.utils.filedump(|\meta{file}|,|\meta{offset}|,|\meta{length}|)| \\ % \end{syntax} % Returns the uppercase hexadecimal representation of the content of the % \meta{file} read as bytes. If the \meta{length} is given, only this part % of the file is returned; similarly, one may specify the \meta{offset} from % the start of the file. If the \meta{length} is not given, the entire file % is read starting at the \meta{offset}. % \end{function} % % \begin{function}{ltx.utils.filemd5sum} % \begin{syntax} % \meta{hash}| = ltx.utils.filemd5sum(|\meta{file}|)| \\ % \end{syntax} % Returns the MD5 sum of the file contents read as bytes; note that % the result will depend on the nature of the line endings used in the file, % in contrast to normal \TeX{} behaviour. If the \meta{file} is not found, % nothing is returned with \emph{no error raised}. % \end{function} % % \begin{function}{ltx.utils.filemoddate} % \begin{syntax} % \meta{date}| = ltx.utils.filemoddate(|\meta{file}|)| \\ % \end{syntax} % Returns the date/time of last modification of the \meta{file} in the % format % \begin{quote} % |D:|\meta{year}\meta{month}\meta{day}\meta{hour}\meta{minute}^^A % \meta{second}\meta{offset} % \end{quote} % where the latter may be |Z| (UTC) or % \meta{plus-minus}\meta{hours}|'|\meta{minutes}|'|. If the \meta{file} is % not found, nothing is returned with \emph{no error raised}. % \end{function} % % \begin{function}{ltx.utils.filesize} % \begin{syntax} % |size = ltx.utils.filesize(|\meta{file}|)| \\ % \end{syntax} % Returns the size of the \meta{file} in bytes. If the \meta{file} is not % found, nothing is returned with \emph{no error raised}. % \end{function} % % \end{documentation} % % \begin{implementation} % % \section{\pkg{l3luatex} implementation} % % \begin{macrocode} %<*package> % \end{macrocode} % % \subsection{Breaking out to \Lua{}} % % \begin{macrocode} %<*tex> % \end{macrocode} % % \begin{macrocode} %<@@=lua> % \end{macrocode} % % \begin{macro}[EXP]{\@@_escape:n, \@@_now:n, \@@_shipout:n} % Copies of primitives. % \begin{macrocode} \cs_new_eq:NN \@@_escape:n \tex_luaescapestring:D \cs_new_eq:NN \@@_now:n \tex_directlua:D \cs_new_eq:NN \@@_shipout:n \tex_latelua:D % \end{macrocode} % \end{macro} % % These functions are set up in \pkg{l3str} for bootstrapping: we want to % replace them with a \enquote{proper} version at this stage, so clean up. % \begin{macrocode} \cs_undefine:N \lua_escape:e \cs_undefine:N \lua_now:e % \end{macrocode} % % \begin{macro}[EXP]{\lua_now:n, \lua_now:e} % \begin{macro}{\lua_shipout_e:n, \lua_shipout:n} % \begin{macro}[EXP]{\lua_escape:n, \lua_escape:e} % Wrappers around the primitives. % \begin{macrocode} \cs_new:Npn \lua_now:e #1 { \@@_now:n {#1} } \cs_new:Npn \lua_now:n #1 { \lua_now:e { \exp_not:n {#1} } } \cs_new_protected:Npn \lua_shipout_e:n #1 { \@@_shipout:n {#1} } \cs_new_protected:Npn \lua_shipout:n #1 { \lua_shipout_e:n { \exp_not:n {#1} } } \cs_new:Npn \lua_escape:e #1 { \@@_escape:n {#1} } \cs_new:Npn \lua_escape:n #1 { \lua_escape:e { \exp_not:n {#1} } } % \end{macrocode} % \end{macro} % \end{macro} % \end{macro} % % \begin{macro}{\lua_load_module:n} % Wrapper around |require'|\meta{module}|'|. % \begin{macrocode} \str_new:N \l_@@_err_msg_str \cs_new_protected:Npn \lua_load_module:n #1 { \bool_if:nF { \@@_load_module_p:n { #1 } } { \msg_error:nnnV { luatex } { module-not-found } { #1 } \l_@@_err_msg_str } } % \end{macrocode} % \end{macro} % % As with engines other than \LuaTeX{} % these have to be macros, we give them the same status in all cases. % When \LuaTeX{} is not in use, simply give an error message/ % \begin{macrocode} \sys_if_engine_luatex:F { \clist_map_inline:nn { \lua_escape:n , \lua_escape:e , \lua_now:n , \lua_now:e } { \cs_gset:Npn #1 ##1 { \msg_expandable_error:nnn { luatex } { luatex-required } { #1 } } } \clist_map_inline:nn { \lua_shipout_e:n , \lua_shipout:n, \lua_load_module:n } { \cs_gset_protected:Npn #1 ##1 { \msg_error:nnn { luatex } { luatex-required } { #1 } } } } % \end{macrocode} % \subsection{Messages} % % \begin{macrocode} \msg_new:nnnn { luatex } { luatex-required } { LuaTeX~engine~not~in~use!~Ignoring~#1. } { The~feature~you~are~using~is~only~available~ with~the~LuaTeX~engine.~LaTeX3~ignored~'#1'. } \msg_new:nnnn { luatex } { module-not-found } { Lua~module~`#1'~not~found. } { The~file~`#1.lua'~could~not~be~found.~Please~ensure~ that~the~file~was~properly~installed~and~that~the~ filename~database~is~current. \\ \\ The~Lua~loader~provided~this~additional~information: \\ #2 } \prop_gput:Nnn \g_msg_module_name_prop { luatex } { LaTeX } \prop_gput:Nnn \g_msg_module_type_prop { luatex } { } % \end{macrocode} % % \begin{macrocode} % % \end{macrocode} % % \subsection{\Lua{} functions for internal use} % % \begin{macrocode} %<*lua> % \end{macrocode} % % Most of the emulation of \pdfTeX{} here is based heavily on Heiko Oberdiek's % \pkg{pdftexcmds} package. % % \begin{macro}{ltx.utils} % Create a table for the kernel's own use. % \begin{macrocode} ltx = ltx or {utils={}} ltx.utils = ltx.utils or { } local ltxutils = ltx.utils % \end{macrocode} % \end{macro} % % Local copies of global tables. % \begin{macrocode} local io = io local kpse = kpse local lfs = lfs local math = math local md5 = md5 local os = os local string = string local tex = tex local texio = texio local tonumber = tonumber % \end{macrocode} % % Local copies of standard functions. % \begin{macrocode} local abs = math.abs local byte = string.byte local floor = math.floor local format = string.format local gsub = string.gsub local lfs_attr = lfs.attributes local open = io.open local os_date = os.date local setcatcode = tex.setcatcode local sprint = tex.sprint local cprint = tex.cprint local write = tex.write local write_nl = texio.write_nl local utf8_char = utf8.char local package_loaded = package.loaded local package_searchers = package.searchers local table_concat = table.concat local scan_int = token.scan_int or token.scan_integer local scan_string = token.scan_string local scan_keyword = token.scan_keyword local put_next = token.put_next local token_create = token.create local token_new = token.new local set_macro = token.set_macro % \end{macrocode} % % Since token.create only returns useful values after the tokens % has been added to TeX's hash table, we define a variant which % defines it first if necessary. % \begin{macrocode} local token_create_safe do local is_defined = token.is_defined local set_char = token.set_char local runtoks = tex.runtoks local let_token = token_create'let' function token_create_safe(s) local orig_token = token_create(s) if is_defined(s, true) then return orig_token end set_char(s, 0) local new_token = token_create(s) runtoks(function() put_next(let_token, new_token, orig_token) end) return new_token end end local true_tok = token_create_safe'prg_return_true:' local false_tok = token_create_safe'prg_return_false:' % \end{macrocode} % In Con\TeX{}t lmtx \texttt{token.command_id} does not exist, % but it can easily be emulated with Con\TeX{}t's \texttt{tokens.commands}. % \begin{macrocode} local command_id = token.command_id if not command_id and tokens and tokens.commands then local id_map = tokens.commands function command_id(name) return id_map[name] end end % \end{macrocode} % % Deal with Con\TeX{}t: doesn't use |kpse| library. % \begin{macrocode} local kpse_find = (resolvers and resolvers.findfile) or kpse.find_file % \end{macrocode} % % \begin{macro}[int]{escapehex} % An internal auxiliary to convert a string to the matching hex escape. % This works on a byte basis: extension to handled UTF-8 input is % covered in \pkg{pdftexcmds} but is not currently required here. % \begin{macrocode} local function escapehex(str) return (gsub(str, ".", function (ch) return format("%02X", byte(ch)) end)) end % \end{macrocode} % \end{macro} % % \begin{macro}{ltx.utils.filedump} % Similar comments here to the next function: read the file in binary mode % to avoid any line-end weirdness. % \begin{macrocode} local function filedump(name,offset,length) local file = kpse_find(name,"tex",true) if not file then return end local f = open(file,"rb") if not f then return end if offset and offset > 0 then f:seek("set", offset) end local data = f:read(length or 'a') f:close() return escapehex(data) end ltxutils.filedump = filedump % \end{macrocode} % \end{macro} % % \begin{macro}[int]{md5.HEX} % Hash a string and return the hash in uppercase hexadecimal format. % In some engines, this is build-in. For traditional \LuaTeX{}, the conversion % to hexadecimal has to be done by us. % \begin{macrocode} local md5_HEX = md5.HEX if not md5_HEX then local md5_sum = md5.sum function md5_HEX(data) return escapehex(md5_sum(data)) end md5.HEX = md5_HEX end % \end{macrocode} % \end{macro} % \begin{macro}{ltx.utils.filemd5sum} % Read an entire file and hash it: the hash function itself is a built-in. % As Lua is byte-based there is no work needed here in terms of UTF-8 % (see \pkg{pdftexcmds} and how it handles strings that have passed through % \LuaTeX{}). The file is read in binary mode so that no line ending % normalisation occurs. % \begin{macrocode} local function filemd5sum(name) local file = kpse_find(name, "tex", true) if not file then return end local f = open(file, "rb") if not f then return end local data = f:read("*a") f:close() return md5_HEX(data) end ltxutils.filemd5sum = filemd5sum % \end{macrocode} % \end{macro} % % \begin{macro}{ltx.utils.filemoddate} % There are two cases: If the C standard library is C99 compliant, % we can use |%z| to get the timezone in almost the right format. % We only have to add primes and replace a zero or missing offset % with |Z|. % % Of course this would be boring, so Windows does things differently. % There we have to manually calculate the offset. % See procedure \texttt{makepdftime} in \texttt{utils.c} of % \pdfTeX{}. % \begin{macrocode} local filemoddate if os_date'%z':match'^[+-]%d%d%d%d$' then local pattern = lpeg.Cs(16 * (lpeg.Cg(lpeg.S'+-' * '0000' * lpeg.Cc'Z') + 3 * lpeg.Cc"'" * 2 * lpeg.Cc"'" + lpeg.Cc'Z') * -1) function filemoddate(name) local file = kpse_find(name, "tex", true) if not file then return end local date = lfs_attr(file, "modification") if not date then return end return pattern:match(os_date("D:%Y%m%d%H%M%S%z", date)) end else local function filemoddate(name) local file = kpse_find(name, "tex", true) if not file then return end local date = lfs_attr(file, "modification") if not date then return end local d = os_date("*t", date) local u = os_date("!*t", date) local off = 60 * (d.hour - u.hour) + d.min - u.min if d.year ~= u.year then if d.year > u.year then off = off + 1440 else off = off - 1440 end elseif d.yday ~= u.yday then if d.yday > u.yday then off = off + 1440 else off = off - 1440 end end local timezone if off == 0 then timezone = "Z" else if off < 0 then timezone = "-" off = -off else timezone = "+" end timezone = format("%s%02d'%02d'", timezone, hours // 60, hours % 60) end return format("D:%04d%02d%02d%02d%02d%02d%s", d.year, d.month, d.day, d.hour, d.min, d.sec, timezone) end end ltxutils.filemoddate = filemoddate % \end{macrocode} % \end{macro} % % \begin{macro}{ltx.utils.filesize} % A simple disk lookup. % \begin{macrocode} local function filesize(name) local file = kpse_find(name, "tex", true) if file then local size = lfs_attr(file, "size") if size then return size end end end ltxutils.filesize = filesize % \end{macrocode} % \end{macro} % % \begin{macro}[int]{luadef} % An internal function for defining control sequences form Lua which behave % like primitives. This acts as a wrapper around |token.set_lua| which accepts % a function instead of an index into the functions table. % \begin{macrocode} local luacmd do local set_lua = token.set_lua local undefined_cs = command_id'undefined_cs' if not context and not luatexbase then require'ltluatex' end if luatexbase then local new_luafunction = luatexbase.new_luafunction local functions = lua.get_functions_table() function luacmd(name, func, ...) local id local tok = token_create(name) if tok.command == undefined_cs then id = new_luafunction(name) set_lua(name, id, ...) else id = tok.index or tok.mode end functions[id] = func end elseif context then local register = context.functions.register local functions = context.functions.known function luacmd(name, func, ...) local tok = token_create(name) if tok.command == undefined_cs then token.set_lua(name, register(func), ...) else functions[tok.index or tok.mode] = func end end end end % \end{macrocode} % \end{macro} % % \begin{macro}[int]{try_require} % Loads a Lua module. This function loads the module similarly to the standard % Lua global function |require|, with a few differences. On success, % |try_require| returns |true, module|. If the module cannot be found, it % returns |false, err_msg|. If the module is found, but something goes wrong % when loading it, the function throws an error. % \begin{macrocode} local function try_require(name) if package_loaded[name] then return true, package_loaded[name] end local failure_details = {} for _, searcher in ipairs(package_searchers) do local loader, data = searcher(name) if type(loader) == 'function' then package_loaded[name] = loader(name, data) or true return true, package_loaded[name] elseif type(loader) == 'string' then failure_details[#failure_details + 1] = loader end end return false, table_concat(failure_details, '\n') end % \end{macrocode} % \end{macro} % % \begin{macro}{\@@_load_module_p:n} % Check to see if we can load a module using |require|. If we % can load the module, then we load it immediately. Otherwise, we save the % error message in |\l_@@_err_msg_str|. % \begin{macrocode} local char_given = command_id'char_given' local c_true_bool = token_create(1, char_given) local c_false_bool = token_create(0, char_given) local c_str_cctab = token_create('c_str_cctab').mode luacmd('@@_load_module_p:n', function() local success, result = try_require(scan_string()) if success then set_macro(c_str_cctab, 'l_@@_err_msg_str', '') put_next(c_true_bool) else set_macro(c_str_cctab, 'l_@@_err_msg_str', result) put_next(c_false_bool) end end) % \end{macrocode} % \end{macro} % % \subsection{Preserving iniTeX Lua data for runs} % % \begin{macrocode} %<@@=lua> % \end{macrocode} % % The Lua state is not dumped when a forat is written, therefore any Lua % variables filled doing format building need to be restored in order to % be accessible during normal runs. % % We provide some kernel-internal helpers for this. They will only be available if % \texttt{luatexbase} is available. This is not a big restriction though, because % Con\TeX{}t (which does not use \texttt{luatexbase}) does not load \pkg{expl3} % in the format. % % \begin{macrocode} local register_luadata, get_luadata if luatexbase then local register = token_create'@expl@luadata@bytecode'.index if status.ini_version then % \end{macrocode} % % \begin{macro}[int]{register_luadata} % \texttt{register_luadata} is only available during format generation. % It accept a string which uniquely identifies the data object and has to be % provided to retrieve it later. Additionally it accepts a function which is % called in the \texttt{pre_dump} callback and which has to return a string that % evaluates to a valid Lua object to be preserved. % \begin{macrocode} local luadata, luadata_order = {}, {} function register_luadata(name, func) if luadata[name] then error(format("LaTeX error: data name %q already in use", name)) end luadata[name] = func luadata_order[#luadata_order + 1] = func and name end % \end{macrocode} % \end{macro} % % The actual work is done in \texttt{pre_dump}. The \texttt{luadata_order} is used % to ensure that the order is consistent over multiple runs. % \begin{macrocode} luatexbase.add_to_callback("pre_dump", function() if next(luadata) then local str = "return {" for i=1, #luadata_order do local name = luadata_order[i] str = format('%s[%q]=%s,', str, name, luadata[name]()) end lua.bytecode[register] = assert(load(str .. "}")) end end, "ltx.luadata") else % \end{macrocode} % % \begin{macro}[int]{get_luadata} % \texttt{get_luadata} is only available if data should be restored. % It accept the identifier which was used when the data object was registered and % returns the associated object. Every object can only be retrieved once. % \begin{macrocode} local luadata = lua.bytecode[register] if luadata then lua.bytecode[register] = nil luadata = luadata() end function get_luadata(name) if not luadata then return end local data = luadata[name] luadata[name] = nil return data end end end % \end{macrocode} % \end{macro} % % \begin{macrocode} % % \end{macrocode} % % \begin{macrocode} % % \end{macrocode} % %\end{implementation} % %\PrintIndex