---library for `luatex`, `lualatex`, `luatexinfo` and `texlua` ---@module texrocks ---@copyright 2025 ---@diagnostic disable: undefined-field -- luacheck: ignore 143 local lfs = require "lfs" local M = { fontmap_name = "luatex.map" } if os.type == "windows" then M.OSFONTDIR = "C:/Windows/System32/Fonts" elseif os.type == "unix" then if os.getenv "XDG_DATA_DIRS" ~= nil then M.OSFONTDIR = "{" .. os.getenv("XDG_DATA_DIRS"):gsub(":", ",") .. "}/share/fonts//" else local prefixes = { "/usr" } if os.getenv "PREFIX" ~= nil then prefixes = { os.getenv "PREFIX" } elseif os.name ~= "cygwin" then table.insert(prefixes, "/usr/local") elseif os.getenv "MINGW_PREFIX" ~= nil then table.insert(prefixes, os.getenv "MINGW_PREFIX") end M.OSFONTDIR = "{" .. table.concat(prefixes, ",") .. "}/share/fonts//" end end if os.name == "macosx" then M.OSFONTDIR = M.OSFONTDIR .. ";{/System,}/Library/Fonts//" elseif os.name == "cygwin" then M.OSFONTDIR = M.OSFONTDIR .. ";/proc/cygdrive/c/Windows/System32/Fonts" end ---base name ---@param path string ---@return string path function M.basename(path) path = path:gsub(".*/", "") return path end ---path without extension name ---@param path string ---@return string path function M.rootname(path) path = path:gsub("%.*", "") return path end ---base name without extension name ---@param path string ---@return string path function M.name(path) return M.rootname(M.basename(path)) end ---get the first non-nil element's index ---@param args string[] index can be negative ---@return integer begin begin index function M.get_begin_index(args) local begin = -1 while args[begin] do begin = begin - 1 end begin = begin + 1 return begin end ---texlua has a behaviour about command line arguments. ---`arg` starts from index 0: `arg = {[0] = "ls", "-al"}` ---`os.exec()` starts from index 1: `os.exec{"ls", "-al"}` ---we need to shift it ---@param args string[] command line arguments ---@param offset integer e.g., `-1` means `args[i + 1] = args[i]` ---@return string[] cmd_args function M.shift(args, offset) local begin = M.get_begin_index(args) local cmd_args = {} for i = begin, #args do cmd_args[i - offset] = args[i] end return cmd_args end ---call `os.setenv()` when environment variable doesn't exist ---@param key string ---@param value string function M.setenv(key, value) if os.getenv(key) == nil then os.setenv(key, value) end end ---get paths from `package.path`/`package.cpath`. see tests. ---@param path string paths concatenated by `;` ---@param suffix string | nil add `../${suffix}//` to paths when it is not nil ---@return string[] paths function M.getpaths(path, suffix) local parts = {} local paths = {} for part in string.gmatch(path, "([^;]+)") do part = part:gsub("/%?.*", "") if not parts[part] then parts[part] = true if suffix then part = part:gsub("/src$", ""):gsub("/lib$", "") .. '/etc/' .. suffix -- for test if lfs.isdir == nil or lfs.isdir(part) then part = part .. "//" table.insert(paths, part) end else table.insert(paths, part) end end end return paths end ---concatenate `getpaths()` ---@param path string same as `getpaths()` ---@param suffix string | nil same as `getpaths()` ---@return string path concatenated by `;` ---@see getpaths function M.getenv(path, suffix) local processed = M.getpaths(path, suffix) return table.concat(processed, ";") end ---**entry for texlua** ---@param args string[] `arg` function M.main(args) M.setenvs() -- progname should be texlua M.setotherenv(M.name(args[0])) -- luacheck: ignore 121 arg = M.preparse(args) loadfile(arg[0])() end ---wrap `os.setenv()` for font files due to `OSFONTDIR` ---@param key string ---@param value string function M.setfontenv(key, value) os.setenv(key, "$TEXMFDOTDIR;" .. M.getenv(package.path, "fonts/" .. value) .. ";" .. M.OSFONTDIR) end ---set environment variables for kpathsea ---@source ../packages/kpathsea/lua/kpathsea.lua function M.setenvs() M.setenv("TEXMFDOTDIR", ".") if os.getenv "USERPROFILE" == nil then M.setenv("HOME", "~") else M.setenv("HOME", os.getenv "USERPROFILE") end -- https://wiki.archlinux.org/title/XDG_Base_Directory#Partial if os.getenv "LOCALAPPDATA" == nil then M.setenv("XDG_CONFIG_HOME", (os.getenv "HOME") .. "/.config") else M.setenv("XDG_CONFIG_HOME", os.getenv "LOCALAPPDATA") end if os.getenv "APPDATA" == nil then M.setenv("XDG_DATA_HOME", (os.getenv "HOME") .. "/.local/share") else M.setenv("XDG_CONFIG_HOME", os.getenv "APPDATA") end if os.getenv "TEMP" == nil then M.setenv("XDG_CACHE_HOME", (os.getenv "HOME") .. "/.cache") else M.setenv("XDG_CACHE_HOME", os.getenv "TEMP") end -- some tex packages like hyperref support config file such as hyperref.cfg M.setenv("TEXMFCONFIG", "$XDG_CONFIG_HOME/texmf") M.setenv("TEXMFHOME", "$XDG_DATA_HOME/texmf") M.setenv("TEXMFVAR", "$XDG_CACHE_HOME/texmf") -- project setting > config > data > cache -- create ./*.cnf to override os.setenv("TEXMF", "$TEXMFDOTDIR;$TEXMFCONFIG;$TEXMFHOME;$TEXMFVAR") -- create ./texmf.cnf to override lua/texrocks/texmf.cnf os.setenv("TEXMFCNF", "$TEXMFDOTDIR;$TEXMFCONFIG;$TEXMFHOME;$TEXMFVAR;" .. debug.getinfo(1).source:match("@?(.*)/") .. '/texrocks') os.setenv("TEXMFDBS", "") os.setenv("LUAINPUTS", "$TEXMFDOTDIR;" .. M.getenv(package.path)) os.setenv("CLUAINPUTS", "$TEXMFDOTDIR;" .. M.getenv(package.cpath)) os.setenv("TEXINPUTS", "$TEXMFDOTDIR;" .. M.getenv(package.path, "tex")) os.setenv("BIBINPUTS", "$TEXMFDOTDIR;" .. M.getenv(package.path, "bibtex/bib")) os.setenv("MLBIBINPUTS", "$TEXMFDOTDIR;" .. M.getenv(package.path, "bibtex/mlbib")) os.setenv("BSTINPUTS", "$TEXMFDOTDIR;" .. M.getenv(package.path, "bibtex/bst")) os.setenv("MLBSTINPUTS", "$TEXMFDOTDIR;" .. M.getenv(package.path, "bibtex/mlbst")) os.setenv("RISINPUTS", "$TEXMFDOTDIR;" .. M.getenv(package.path, "biber/ris")) os.setenv("BLTXMLINPUTS", "$TEXMFDOTDIR;" .. M.getenv(package.path, "biber/bltxml")) os.setenv("TEXINDEXSTYLE", "$TEXMFDOTDIR;" .. M.getenv(package.path, "makeindex")) os.setenv("MFTINPUTS", "$TEXMFDOTDIR;" .. M.getenv(package.path, "mft")) os.setenv("MPINPUTS", "$TEXMFDOTDIR;" .. M.getenv(package.path, "mp")) os.setenv("OCPINPUTS", "$TEXMFDOTDIR;" .. M.getenv(package.path, "omega/ocp")) os.setenv("OTPINPUTS", "$TEXMFDOTDIR;" .. M.getenv(package.path, "omega/otp")) os.setenv("WEBINPUTS", "$TEXMFDOTDIR;" .. M.getenv(package.path, "web")) os.setenv("CWEBINPUTS", "$TEXMFDOTDIR;" .. M.getenv(package.path, "cweb")) os.setenv("TEXFORMATS", "$TEXMFDOTDIR;" .. M.getenv(package.path, "web2c")) os.setenv("TEXDOCS", "$TEXMFDOTDIR;" .. M.getenv(package.path, "doc")) os.setenv("TEXSOURCES", "$TEXMFDOTDIR;" .. M.getenv(package.path, "source")) os.setenv("MFINPUTS", "$TEXMFDOTDIR;" .. M.getenv(package.path, "fonts/source")) os.setenv("MPSUPPORT", "$TEXMFDOTDIR;" .. M.getenv(package.path, "metapost/support")) os.setenv("TEXPICTS", "$TEXMFDOTDIR;" .. M.getenv(package.path, "images")) os.setenv("TEXPOOL", "$TEXMFDOTDIR;" .. M.getenv(package.path, "web2c")) os.setenv("TEXPSHEADERS", "$TEXMFDOTDIR;" .. M.getenv(package.path, "dvips")) os.setenv("WEB2C", "$TEXMFDOTDIR;" .. M.getenv(package.path, "web2c")) os.setenv("TEXMFSCRIPTS", "$TEXMFDOTDIR;" .. M.getenv(package.path, "scripts")) os.setenv("TEXCONFIG", "$TEXMFDOTDIR;" .. M.getenv(package.path, "conf/dvips")) os.setenv("PDFTEXCONFIG", "$TEXMFDOTDIR;" .. M.getenv(package.path, "conf/pdftex")) os.setenv("TEXFONTMAPS", ".lux;$XDG_DATA_HOME/lux/tree") -- font metrics M.setfontenv("TFMFONTS", "tfm") M.setfontenv("OFMFONTS", "ofm") -- luatex M.setfontenv("T1FONTS", "type1") M.setfontenv("OVFFONTS", "ovf") M.setfontenv("OVPFONTS", "ovp") M.setfontenv("VFFONTS", "vf") -- luahbtex M.setfontenv("TTFONTS", "truetype") M.setfontenv("OPENTYPEFONTS", "opentype") -- other fonts -- /usr/share/groff/{current/font,site-font}/devps M.setfontenv("TRFONTS", "groff") M.setfontenv("GFFONTS", "gf") M.setfontenv("PKFONTS", "pk") M.setfontenv("OPLFONTS", "opl") M.setfontenv("T42FONTS", "type42") M.setfontenv("MISCFONTS", "misc") M.setfontenv("ENCFONTS", "enc") M.setfontenv("CMAPFONTS", "cmap") M.setfontenv("SFDFONTS", "sfd") M.setfontenv("LIGFONTS", "lig") M.setfontenv("FONTFEATURES", "fea") M.setfontenv("FONTCIDMAPS", "cid") end ---set environment variables for `kpsewhich --show-path 'other text files'` ---@param progname string read function M.setotherenv(progname) M.setenv(progname:upper() .. "INPUTS", "$TEXMFDOTDIR;" .. M.getenv(package.path, "conf")) end ---get offset from one script to another script ---such as `texlua --option main.lua --option` -> `main.lua --option` ---offset should be 2 ---@param args string[] command line arguments ---@return integer offset function M.get_offset(args) local offset for i, v in ipairs(args) do local char = v:sub(1, 1) -- skip \macro and --option if char ~= "\\" and char ~= "-" then offset = i break end end return offset end ---refer `parse` ---@see parse ---@param args string[] command line arguments ---@return string[] cmd_args parsed result function M.preparse(args) local offset = M.get_offset(args) if offset == nil then error("haven't support") os.exit(1) end return M.shift(args, offset) end ---**entry for luatex** ---@param args string[] `arg` function M.run(args) local cmd_args = M.parse(args) M.setotherenv(M.get_program_name(cmd_args)) M.sync(false) M.exec(cmd_args) end ---luahbtex --luaonly texlua luatex: ---texlua will call preparse(), then loadfile("luatex")() ---luatex will call parse(), then os.exec{[0]="luatex", "luahbtex"} ---@param args string[] command line arguments ---@return string[] cmd_args parsed result ---@see preparse function M.parse(args) local cmd_args = M.shift(args, -1) local begin = M.get_begin_index(cmd_args) cmd_args[0] = cmd_args[begin] return cmd_args end ---see 's command line options ---@param args string[] command line arguments not `arg` ---@return string progname function M.get_program_name(args) -- --progname is latter first for i = #args, 2, -1 do if args[i]:match("^--progname=") then local progname = args[i]:gsub("^--progname=", "") return progname elseif args[i - 1] == "--progname" then return args[i] end end -- --fmt/--ini is former first local opt for i = 2, #args do if args[i]:match("^--fmt=") then local progname = args[i]:gsub("^--fmt=", "") return progname elseif args[i] == "--fmt" or args[i] == "--ini" then opt = args[i] elseif args[i]:match("^%-") == args[i]:match("^\\") then if opt == "--fmt" then return args[i] elseif opt == "--ini" then local progname = args[i]:gsub(".*/", ""):gsub("%.*", "") return progname end end end -- usually be luahbtex return M.name(args[1]) end ---update font map file: `.lux/luatex.map` ---@param short boolean use relative (short)/absolute (long) path for font files function M.sync(short) local dir = ".lux" if not lfs.isdir(dir) then lfs.mkdir(dir) end local fontmap_name = dir .. "/" .. M.fontmap_name local f = io.open(fontmap_name, 'w') if f == nil then print("fail to generate " .. fontmap_name) return end local fonts = {} local function walk(path) for file in lfs.dir(path) do if file:sub(1, 1) ~= '.' then local newpath = path .. '/' .. file if lfs.isdir(newpath) then walk(newpath) elseif lfs.isfile(newpath) then local ext = file:gsub(".*%.", "") if ext == "pfb" or ext == "t3" then table.insert(fonts, newpath) end end end end end for _, path in ipairs(M.getpaths(package.path, "fonts")) do walk(path:gsub("//$", "")) end local lines = {} for _, font in ipairs(fonts) do local basename = font:gsub(".*/", '') local name = basename:gsub("%..*", '') local path = font if short then path = basename end table.insert(lines, string.format("%s %s <%s", name, name:upper(), path)) end local template = debug.getinfo(1).source:match("@?(.*)/") .. '/texrocks/' .. M.fontmap_name local t = io.open(template) if t then f:write(t:read("*a")) t:close() end f:write(table.concat(lines, "\n")) f:close() end ---texlua's `os.exec()` will not exec when meet error ---we wrap it to add `os.exit()` ---@param args string[] command line arguments function M.exec(args) local _, msg, code = os.exec(args) error(msg) -- 2: No such file or directory -- nil: invalid command line passed os.exit(code or 1) end return M