---library for `texdef` and `latexdef` ---@module texdef ---@copyright 2026 local tex = require 'tex' local kpse = require 'kpse' local utils = require 'prompt.utils' local argparse = require 'argparse' local minijinja = require 'minijinja' local M = { templates = {}, } ---https://github.com/lumen-oss/lux/issues/922 ---@param name string ---@return string function M.read(name) local root = debug.getinfo(1).source:match("@?(.*).lua$") local f = io.open(root .. "/templates/" .. name) local content = "" if f then content = f:read("*a") f:close() end return content end for _, name in ipairs { "main.tex", "sub.tex" } do M.templates[name] = M.read(name) end ---get parser ---@param progname string program name ---@param formatname string TeX format name ---@return table parser function M.get_parser(progname, formatname) local parser = argparse(progname):add_complete() parser:argument('macro', 'macro name without \\'):args('*') parser:option('--value -v', [[Show value of \the\macro instead]]):args(0) local hidden = formatname:match 'latex' == nil parser:option('--list -l', 'List all command sequences of the given packages by -l, -ll'):args(0):count("*") :hidden(hidden) parser:option('--find -f', 'Show full filepath of the file where the command sequence was defined by -f, -ff') :args(0):count("*") :hidden(hidden) parser:option('--ignore-regex -I', 'Ignore all command sequences in the above lists which match lua match(), use ""', '[@_]'):convert(M.convert_regex) :hidden(hidden) parser:option('--Environment -E', 'Every command name is taken as an environment name'):args(0) :hidden(hidden) parser:option('--class -c', 'class name', not hidden and 'article'):convert(M.add_brace) :hidden(hidden) parser:option('--package -p', 'package name'):count("*"):convert(M.add_braces) :hidden(hidden) parser:option('--environment -e', 'environment name'):count("*"):convert(M.add_braces) :hidden(hidden) parser:option('--othercode -o', 'Add other code into the preamble before the definition is shown'):count("*") :hidden(hidden) parser:option('--preamble -P', 'Show definition of the command inside the preamble'):args(0) :hidden(hidden) parser:option('--beforeclass -B', [[Show definition of the command before \documentclass]]):args(0) :hidden(hidden) parser:option('--before -b', 'Place code before definition is shown'):count("*") parser:option('--after -a', 'Place code after definition is shown'):count("*") parser:option('--dry-run -n', 'Do not run'):args(0) parser:option('--output', 'output file name', tex.jobname .. '.tex') parser:option('--entering', 'entering file prompt', '>> entering file ') parser:option('--leaving', 'leaving file prompt', '<< leaving file ') parser:option('--defined', 'defined prompt', ': defined by ') parser:option('--formatname', [[tex.formatname]], formatname) return parser end --- '' -> '$^' ---@param name string ---@return string function M.convert_regex(name) return name == '' and '$^' or name end ---add brace: article -> {article} ---@param name string? ---@return string? function M.add_brace(name) if name and name:sub(#name, #name) ~= '}' then name = '{' .. name .. '}' end return name end ---add braces: hyperref -> {hyperref} ---@param names string[]? ---@return string[]? function M.add_braces(names) if names then for i, name in pairs(names) do names[i] = M.add_brace(name) end end return names end ---parse command line arguments ---@param argv string[] command line arguments ---@return table args parsed result function M.parse(argv) local args = utils.parse(argv) local parser = M.get_parser(args[0], tex.formatname) args = parser:parse(args) if args.Environment then for i = 1, #args.macro do table.insert(args.macro, 'end' .. args.macro[i]) end end return args end ---get environment for minijinja ---@return table function M.get_environment() local env = minijinja.Environment:new() for name, template in pairs(M.templates) do env:add_template(name, template) end return env end ---**first entry for texdef and latexdef** ---@param argv string[] command line arguments ---@return table? args parsed command line arguments function M.main(argv) print() local args = M.parse(argv) local env = M.get_environment() local code = env:render_template('main.tex', args) if args.dry_run then print(code) return end local output = args.output if args.list > 0 then output = tex.jobname .. '.log' end args.f = io.open('.lux/' .. output, 'w+') if args.f then code = code:gsub("^%s+", ""):gsub("\n", "") tex.print(code) end return args end ---replace package names with their full paths ---@param text string ---@param defined string ---@return string text function M.replace(text, defined) local paths = {} for file in text:gmatch(defined .. '(%S+)') do paths[file] = kpse.lookup(file) end for file, path in pairs(paths) do text = text:gsub(defined .. file, defined .. path) end return text end ---@alias cs {type: '=' | '->' | '-->', value: string} ---@alias pkg table ---@alias log table ---extract packages' macros' information from log ---one log contains many packages, one package contains many control sequences ---control sequence can be `k = v`, `k -> v` (macro), `k --> v` (long macro) ---@param f table log file handler ---@param entering string entering prompt ---@param leaving string leaving prompt ---@return log log function M.parse_log(f, entering, leaving) local log = {} local pkg_names = {} local pkg_name for line in f:lines() do if line:match(entering) then pkg_name = line:gsub(entering, '') table.insert(pkg_names, pkg_name) log[pkg_name] = {} elseif pkg_name and line:match(leaving .. pkg_name) then table.remove(pkg_names) pkg_name = table.remove(pkg_names) if pkg_name then table.insert(pkg_names, pkg_name) end elseif pkg_name and (line:sub(2):match('^into ') or line:sub(2):match('^reassigning ')) then line = line:sub(2, #line - 1):gsub("^%S+ ", ""):gsub("\\ETC%.", "..."):gsub( 'used in a moving argument.', '(moving)') local cs_name = line:match("^[^=]+") local cs = { type = '=', value = line:gsub("[^=]+=", ""), } if cs.value:match("^\\long macro:") then cs.type = '-->' cs.value = cs.value:gsub("^\\long macro:", "") elseif cs.value:match("^macro:") then cs.type = '->' cs.value = cs.value:gsub("^macro:", "") end if cs.type ~= '=' then local name = cs.value:gsub("%s*->.*", "") cs_name = cs_name .. name cs.value = cs.value:gsub(name .. '%s*->', "") end log[pkg_name][cs_name] = cs end end return log end ---filter log by regex ---@param log log ---@param regex string ---@return log log function M.filter(log, regex) for pkg_name, pkg in pairs(log) do for cs_name, _ in pairs(pkg) do if cs_name:match(regex) then log[pkg_name][cs_name] = nil end end end return log end ---sort dictionary's keys ---@param input table ---@return table names function M.get_sorted_keys(input) local names = {} for name, _ in pairs(input) do table.insert(names, name) end table.sort(names) return names end ---sort log and dump output ---@param log log ---@param is_detailed boolean if print control sequences' values ---@return string text function M.dump(log, is_detailed) local pkg_names = M.get_sorted_keys(log) local lines = {} for _, pkg_name in ipairs(pkg_names) do local pkg = log[pkg_name] local cs_names = M.get_sorted_keys(pkg) local sublines = {} for _, cs_name in ipairs(cs_names) do local cs = pkg[cs_name] local line = cs_name if is_detailed then line = line .. ' ' .. cs.type .. ' ' .. cs.value end table.insert(sublines, line) end if #cs_names ~= 0 then table.insert(lines, pkg_name) table.insert(lines, table.concat(sublines, "\n")) end end return table.concat(lines, "\n\n") end ---**final entry for texdef and latexdef** ---@param args table parsed command line arguments function M.output(args) if args == nil or args.f == nil then return end local text if args.list ~= 0 then local log = M.parse_log(args.f, args.entering, args.leaving) log = M.filter(log, args.ignore_regex) text = M.dump(log, args.list > 1) else text = args.f:read("*a"):gsub('=\n', ' = '):gsub('= macro:%->', '-> ') if args.find > 1 then text = M.replace(text, args.defined) end end print(text) args.f:close() end return M