summaryrefslogtreecommitdiff
path: root/support/texdoc/script
diff options
context:
space:
mode:
authorNorbert Preining <norbert@preining.info>2019-09-02 13:46:59 +0900
committerNorbert Preining <norbert@preining.info>2019-09-02 13:46:59 +0900
commite0c6872cf40896c7be36b11dcc744620f10adf1d (patch)
tree60335e10d2f4354b0674ec22d7b53f0f8abee672 /support/texdoc/script
Initial commit
Diffstat (limited to 'support/texdoc/script')
-rwxr-xr-xsupport/texdoc/script/texdoc.tlu17
-rw-r--r--support/texdoc/script/texdoclib-alias.tlu127
-rw-r--r--support/texdoc/script/texdoclib-cli.tlu219
-rw-r--r--support/texdoc/script/texdoclib-config.tlu559
-rw-r--r--support/texdoc/script/texdoclib-const.tlu158
-rw-r--r--support/texdoc/script/texdoclib-score.tlu293
-rw-r--r--support/texdoc/script/texdoclib-search.tlu771
-rw-r--r--support/texdoc/script/texdoclib-util.tlu127
-rw-r--r--support/texdoc/script/texdoclib-view.tlu246
-rw-r--r--support/texdoc/script/texdoclib.tlu37
10 files changed, 2554 insertions, 0 deletions
diff --git a/support/texdoc/script/texdoc.tlu b/support/texdoc/script/texdoc.tlu
new file mode 100755
index 0000000000..b859868236
--- /dev/null
+++ b/support/texdoc/script/texdoc.tlu
@@ -0,0 +1,17 @@
+#!/usr/bin/env texlua
+
+-- texdoc.tlu: the main program of texdoc
+--
+-- The TeX Live Team, GPLv3, see texdoclib.tlu for details
+
+-- Note: we keep this file small as much as possible so that make it easier
+-- to install a new version of texdoc in TEXMFHOME.
+
+-- setup the kpse library and load texdoclib
+kpse.set_program_name(arg[-1], 'texdoc')
+local texdoc = require('texdoclib')
+
+-- execute
+texdoc.cli.exec()
+
+-- vim: ft=lua:
diff --git a/support/texdoc/script/texdoclib-alias.tlu b/support/texdoc/script/texdoclib-alias.tlu
new file mode 100644
index 0000000000..16c375b3ba
--- /dev/null
+++ b/support/texdoc/script/texdoclib-alias.tlu
@@ -0,0 +1,127 @@
+-- texdoclib-alias.tlu: alias handling for texdoc
+--
+-- The TeX Live Team, GPLv3, see texdoclib.tlu for details
+
+--[[ structure of the alias table
+
+alias = {
+ name1 = {<true or nill> stop, <aliasentry> aliasentry1, ...},
+ ...
+}
+stop == true means further alias directives should be ignored
+
+aliasentry = {
+ name = <string> pattern to be matched,
+ score = <number or nil> associated score,
+ original = <true or nil> is this the original keyword?,
+ locale = <true or nil> is this entry found via config.lang?
+}
+score == nil means to use the default score (defined in texdoclib-score.tlu)
+
+--]]
+
+-- dependencies
+local texdoc = {
+ config = require('texdoclib-config'),
+}
+
+-- shortcuts
+local M = {}
+
+-- alias is local to this file
+local alias = {}
+
+-- turn a name into a suitable alias entry
+-- if score is 'false', this is the original name
+local function make_alias(pat, score)
+ local al = {}
+ al.name = pat
+ if score == false then
+ al.original = true
+ else
+ al.score = score -- may be nil
+ end
+ return al
+end
+
+-- add an alias value for a key
+local function add_alias(key, value, score)
+ local k = string.lower(key)
+ alias[k] = alias[k] or {make_alias(key, false)}
+ if alias[k].stop then return end
+ table.insert(alias[k], make_alias(value, score))
+end
+
+-- prevent a key from being further aliased
+local function stop_alias(key)
+ local k = string.lower(key)
+ alias[k] = alias[k] or {}
+ alias[k].stop = true
+end
+
+-- get patterns for a name
+function M.get_patterns(name, no_alias)
+ local n = string.lower(name)
+
+ -- get normal aliases
+ local res
+ if alias[n] and not no_alias then
+ res = alias[n]
+ else
+ res = {make_alias(name, false)}
+ end
+
+ -- check for language-specific aliases
+ local config_lang = texdoc.config.get_value('lang')
+ local lang = config_lang and alias[n .. '-' .. config_lang]
+ if lang then
+ for _, entry in ipairs(lang) do
+ if not entry.original then
+ table.insert(res, {
+ name = entry.name,
+ score = entry.score,
+ locale = true,
+ })
+ end
+ end
+ end
+
+ return res
+end
+
+-- interpret a confline as an alias setting or return false
+function M.confline_to_alias(line, file, pos)
+ -- alias directive without score
+ local key, val = string.match(line, '^alias%s+([%w%p]+)%s*=%s*(.+)')
+ if key and val then
+ add_alias(key, val)
+ return true
+ end
+ -- alias directive with score
+ local score, key, val = string.match(line,
+ '^alias%(([%d+-.]+)%)%s+([%w%p]+)%s*=%s*(.+)')
+ if score then score = tonumber(score) end
+ if key and val and score then
+ add_alias(key, val, score)
+ return true
+ end
+ -- stopalias directive
+ local key = string.match(line, '^stopalias%s+(.+)')
+ if key then
+ stop_alias(key)
+ return true
+ end
+ return false
+end
+
+-- iterator over the list of keys in the alias table
+-- Note: currently this function is not used (for debug)
+function M.aliased_names()
+ return function(_, cur)
+ return (next(alias, cur))
+ end
+end
+
+return M
+
+-- vim: ft=lua:
diff --git a/support/texdoc/script/texdoclib-cli.tlu b/support/texdoc/script/texdoclib-cli.tlu
new file mode 100644
index 0000000000..3de786ef2c
--- /dev/null
+++ b/support/texdoc/script/texdoclib-cli.tlu
@@ -0,0 +1,219 @@
+-- texdoclib-cli.tlu: command line interfaces for texdoc
+--
+-- The TeX Live Team, GPLv3, see texdoclib.tlu for details
+
+-- dependencies
+local texdoc = {
+ const = require('texdoclib-const'),
+ util = require('texdoclib-util'),
+ config = require('texdoclib-config'),
+ search = require('texdoclib-search'),
+ view = require('texdoclib-view'),
+}
+
+-- shortcuts
+local M = {}
+local C = texdoc.const
+local err_print = texdoc.util.err_print
+
+-------------------------- parsing options --------------------------
+
+-- modified Alternative GetOpt
+-- cf. http://lua-users.org/wiki/AlternativeGetOpt
+local function getopt(arg, options)
+ local tmp
+ local tab = {}
+ local saved_arg = {table.unpack(arg)}
+
+ for k, v in ipairs(saved_arg) do
+ if string.sub(v, 1, 2) == '--' then
+ table.remove(arg, 1)
+ local x = string.find(v, '=', 1, true)
+ if x then
+ table.insert(tab, {string.sub(v, 3, x-1), string.sub(v, x+1)})
+ else
+ table.insert(tab, {string.sub(v, 3), true})
+ end
+ elseif string.sub(v, 1, 1) == '-' then
+ table.remove(arg, 1)
+ local y = 2
+ local l = string.len(v)
+ local jopt
+ while (y <= l) do
+ jopt = string.sub(v, y, y)
+ if string.find(options, jopt, 1, true) then
+ if y < l then
+ tmp = string.sub(v, y+1)
+ y = l
+ else
+ table.remove(arg, 1)
+ tmp = saved_arg[k + 1]
+ end
+ if string.match(tmp, '^%-') then
+ table.insert(tab, {jopt, false})
+ else
+ table.insert(tab, {jopt, tmp})
+ end
+ else
+ table.insert(tab, {jopt, true})
+ end
+ y = y + 1
+ end
+ end
+ end
+
+ return tab
+end
+
+local function parse_options()
+ local curr_arg
+ local action = true
+ local cl_config = {}
+
+ local function insert_cl_config(key, val, opt_name)
+ table.insert(cl_config, {key, val, opt_name})
+ end
+
+ -- actual parsing
+ opts = getopt(arg, 'cd')
+
+ for _, tp in ipairs(opts) do
+ local k, v = tp[1], tp[2]
+ if #k == 1 then
+ curr_arg = '-' .. k
+ else
+ curr_arg = '--' .. k
+ end
+
+ -- action
+ if (curr_arg == '-h') or (curr_arg == '--help') then
+ action = 'help'
+ elseif (curr_arg == '-V') or (curr_arg == '--version') then
+ action = 'version'
+ elseif (curr_arg == '-f') or (curr_arg == '--files') then
+ action = 'files'
+ elseif curr_arg == '--just-view' then
+ action = 'view'
+
+ -- mode
+ elseif (curr_arg == '-w') or (curr_arg == '--view') then
+ insert_cl_config('mode', 'view', curr_arg)
+ elseif (curr_arg == '-m') or (curr_arg == '--mixed') then
+ insert_cl_config('mode', 'mixed', curr_arg)
+ elseif (curr_arg == '-l') or (curr_arg == '--list') then
+ insert_cl_config('mode', 'list', curr_arg)
+ elseif (curr_arg == '-s') or (curr_arg == '--showall') then
+ insert_cl_config('mode', 'showall', curr_arg)
+
+ -- interaction
+ elseif (curr_arg == '-I') or (curr_arg == '--nointeract') then
+ insert_cl_config('interact_switch', 'false', curr_arg)
+ elseif (curr_arg == '-i') or (curr_arg == '--interact') then
+ insert_cl_config('interact_switch', 'true', curr_arg)
+
+ -- output format
+ elseif (curr_arg == '-M') or (curr_arg == '--machine') then
+ insert_cl_config('machine_switch', 'true', curr_arg)
+
+ -- config
+ elseif curr_arg == '-c' then
+ local item, value = string.match(v, '^([%a%d_]+)%s*=%s*(.+)')
+ insert_cl_config(item, value, curr_arg)
+
+ -- debug
+ elseif (curr_arg == '-d') or (curr_arg == '--debug') then
+ if v == true then v = 'all' end
+ insert_cl_config('debug_list', v, curr_arg)
+ elseif curr_arg == '-D' then
+ insert_cl_config('debug_list', 'all', curr_arg)
+
+ -- verbosity
+ elseif (curr_arg == '-q') or (curr_arg == '--quiet') then
+ insert_cl_config('verbosity_level', C.min_verbosity, curr_arg)
+ elseif (curr_arg == '-v') or (curr_arg == '--verbose') then
+ insert_cl_config('verbosity_level', C.max_verbosity, curr_arg)
+
+ -- having trouble
+ else
+ err_print('error', 'unknown option: ' .. curr_arg)
+ err_print('error', C.error_msg)
+ return false
+ end
+ end
+
+ return action, cl_config
+end
+
+-------------------------- process execution --------------------------
+
+-- handling actions
+local function do_action(action)
+ if action == 'help' then
+ texdoc.util.print_usage()
+ os.exit(C.exit_ok)
+ elseif action == 'version' then
+ print(string.format(
+ '%s %s (%s)', C.progname, C.version, C.release_date) ..
+ '\n\n' .. C.copyright_msg)
+ os.exit(C.exit_ok)
+ elseif action == 'files' then
+ print(C.fullname .. ' ' .. C.version)
+ texdoc.config.show_config_files(true)
+ os.exit(C.exit_ok)
+ elseif action == 'view' then
+ if not arg[1] then
+ err_print('error', 'Missing file operand to --just-view.')
+ err_print('error', C.error_msg)
+ os.exit(C.exit_usage)
+ end
+ texdoc.view.view_file(arg[1])
+ os.exit(C.exit_ok)
+ end
+end
+
+-- the main loop
+local function do_texdoc()
+ texdoc.search.init_databases()
+
+ for _, docname in ipairs(arg) do
+ -- do we have more then one argument?
+ local multiarg = not not arg[2]
+ -- get results
+ local doclist = texdoc.search.get_doclist(docname)
+ -- deliver results to the user
+ texdoc.view.deliver_results(docname, doclist, multiarg)
+ end
+end
+
+-------------------------- the main function --------------------------
+
+function M.exec()
+ -- parsing command line options
+ local action, cl_config = parse_options()
+
+ if not action then
+ os.exit(C.exit_usage)
+ end
+
+ -- setup config and alias
+ texdoc.config.setup_config_and_alias(cl_config)
+
+ -- special action
+ do_action(action)
+
+ -- do we actually have arguments?
+ if not arg[1] then
+ err_print('error', 'No action specified.')
+ err_print('error', C.error_msg)
+ os.exit(C.exit_usage)
+ end
+
+ -- the main feature
+ do_texdoc()
+
+ os.exit(C.exit_ok)
+end
+
+return M
+
+-- vim: ft=lua:
diff --git a/support/texdoc/script/texdoclib-config.tlu b/support/texdoc/script/texdoclib-config.tlu
new file mode 100644
index 0000000000..471fef9fdc
--- /dev/null
+++ b/support/texdoc/script/texdoclib-config.tlu
@@ -0,0 +1,559 @@
+-- texdoclib-config.tlu: handling config of texdoc
+--
+-- The TeX Live Team, GPLv3, see texdoclib.tlu for details
+
+-- shortcuts
+local M = {}
+local C = require('texdoclib-const')
+
+-- config is local to this file
+local config = {}
+
+-------------------------- handling dependencies --------------------------
+
+-- in this file, dependencies should be required ondemand to prevent infinite
+-- recursion.
+
+local texdoc = {}
+
+-- import specified function from the submodule
+local function import_function(mod, func)
+ texdoc[mod] = texdoc[mod] or require('texdoclib-' .. mod)
+ return texdoc[mod][func]
+end
+
+-------------------------- hide the config table --------------------------
+
+-- config is read-only (but not "deep" read-only)
+local function set_read_only(table, name)
+ assert(next(table) == nil,
+ 'Internal error: ' .. name .. ' should be empty at this point.')
+ local ro = 'Internal error: attempt to update read-only table '
+ local real = {}
+ setmetatable(table, {
+ __index = real,
+ __newindex = function() error(ro .. name .. '.') end,
+ })
+ return function(k, v) real[k] = v end
+end
+local real_set_config = set_read_only(config, 'config')
+
+-- the accessor
+function M.get_value(key) return config[key] end
+
+------------------------- general config functions ------------------------
+
+-- interpreting 'context' in this section
+local function context_to_string(context)
+ if not context then return '(no context)' end
+ if context.src == 'cl' then
+ return 'from command line option "' .. context.name .. '"'
+ elseif context.src == 'env' then
+ return 'from environment variable "' .. context.name .. '"'
+ elseif context.src == 'loc' then
+ return 'from operating system locale'
+ elseif context.src == 'file' then
+ return 'in file "' .. context.file .. '" on line ' .. context.line
+ elseif context.src == 'def' then
+ return 'from built-in defaults'
+ else
+ return 'from unkown source (should not happen, please report)'
+ end
+end
+
+-- a helper function for warning messages in this section
+local function config_warn(key, value, context, unknown)
+ local err_print = import_function('util', 'err_print')
+ local begin = unknown
+ and string.format('Unknown option "%s"', key)
+ or string.format('Illegal value "%s" for option "%s"',
+ key, tostring(value))
+ texdoc.util.err_print('warning',
+ '%s %s. Skipping.', begin, context_to_string(context))
+end
+
+-- set a config parameter, but don't overwrite it if already set
+-- three special types: *_list (list), *_switch (boolean), *_level (number)
+function M.set_config_element(key, value, context)
+ local dbg_print = import_function('util', 'dbg_print')
+ local parse_error = false
+
+ local is_known = false -- is key a valid option?
+ local option
+ for _, option in ipairs(C.known_options) do
+ if string.match(key, '^' .. option .. '$') then
+ is_known = true
+ break
+ end
+ end
+
+ -- warn and exit if key is not a known option
+ if not is_known then config_warn(key, nil, context, true) return end
+
+ -- exit if key is already set (/!\ must test for nil, not false)
+ if not (config[key] == nil) then
+ if context.src ~= 'def' then
+ dbg_print('config', 'Ignoring "%s=%s" %s.',
+ key, tostring(value), context_to_string(context))
+ end
+ return nil
+ end
+
+ -- record the source of the setting
+ real_set_config(key .. '_src', context.src)
+
+ -- detect the type of the key
+ if string.match(key, '_list$') then
+ -- coma-separated list
+ local values
+ if value == '' then
+ values = {}
+ else
+ values = string.explode(value, ',')
+ end
+
+ local inverse = {}
+ for i, j in ipairs(values) do -- sanitize values...
+ j = string.gsub(j, '%s*$', '')
+ j = string.gsub(j, '^%s*', '')
+ values[i] = j
+ inverse[j] = i -- ... and build inverse mapping on the way
+ end
+
+ real_set_config(key, values)
+ real_set_config(key .. '_inv', inverse)
+ real_set_config(key .. '_max', #values)
+ elseif string.find(key, '_switch$') then
+ -- boolean
+ if value == 'true' then
+ real_set_config(key, true)
+ elseif value == 'false' then
+ real_set_config(key, false)
+ else
+ config_warn(key, value, context)
+ parse_error = true
+ end
+ elseif string.find(key, '_level$') then
+ -- integer
+ local val = tonumber(value)
+ if val then
+ real_set_config(key, val)
+ else
+ config_warn(key, value, context)
+ parse_error = true
+ end
+ else -- string
+ real_set_config(key, value)
+ end
+
+ -- special case: if we just set debug_list, print version info now
+ if key == 'debug_list' then
+ dbg_print('version', '%s v%s', C.fullname, C.version)
+ end
+
+ -- now tell what we have just done, for debugging
+ if not parse_error then
+ dbg_print('config', 'Setting "%s=%s" %s.',
+ key, tostring(value), context_to_string(context))
+ end
+end
+local set_config_element = M.set_config_element
+
+-- set a whole list, also without overwriting
+local function set_config_list(conf, context)
+ for key, value in pairs(conf) do
+ set_config_element(key, value, context)
+ end
+end
+
+------------------------ config from command line ------------------------
+
+-- set config from the command line
+-- Note: Make sure to set a default value in setup_config_from_defaults()
+-- if relevant.
+local function setup_config_from_cl(cl_config)
+ for _, e in ipairs(cl_config) do
+ set_config_element(e[1], e[2], {src='cl', name=e[3]})
+ end
+end
+
+------------------------- config from environment --------------------------
+
+-- set config from environment if available
+local function setup_config_from_env()
+ local function set_config_elt_from_vars(key, vars)
+ for _, var in ipairs(vars) do
+ local value = os.getenv(var)
+ if value then
+ set_config_element(key, value, {src='env', name=var})
+ end
+ end
+ end
+ set_config_elt_from_vars('viewer_pdf',
+ {'PDFVIEWER_texdoc', 'PDFVIEWER', 'TEXDOCVIEW_pdf', 'TEXDOC_VIEWER_PDF'})
+ set_config_elt_from_vars('viewer_ps',
+ {'PSVIEWER_texdoc', 'PSVIEWER', 'TEXDOCVIEW_ps', 'TEXDOC_VIEWER_PS'})
+ set_config_elt_from_vars('viewer_dvi',
+ {'DVIVIEWER_texdoc', 'DVIVIEWER', 'TEXDOCVIEW_dvi', 'TEXDOC_VIEWER_DVI'})
+ set_config_elt_from_vars('viewer_html',
+ {'BROWSER_texdoc', 'BROWSER', 'TEXDOCVIEW_html', 'TEXDOC_VIEWER_HTML'})
+ set_config_elt_from_vars('viewer_md',
+ {'MDVIEWER_texdoc', 'MDVIEWER', 'TEXDOCVIEW_md', 'TEXDOC_VIEWER_MD'})
+ set_config_elt_from_vars('viewer_txt',
+ {'PAGER_texdoc', 'PAGER', 'TEXDOCVIEW_txt', 'TEXDOC_VIEWER_TXT'})
+end
+
+---------------------- options and aliases from files ----------------------
+
+-- interpret a confline as a config setting or return false
+local function confline_to_config(line, file, pos)
+ local key, val = string.match(line, '^([%a%d_]+)%s*=%s*(.+)')
+ if key and val then
+ set_config_element(key, val, {src='file', file=file, line=pos})
+ return true
+ end
+ return false
+end
+
+-- set config and aliases from a particular config file assumed to exist
+local function read_config_file(configfile)
+ local err_print = import_function('util', 'err_print')
+ local confline_to_alias = import_function('alias', 'confline_to_alias')
+ local confline_to_score = import_function('score', 'confline_to_score')
+
+ local cnf = assert(io.open(configfile, 'r'))
+ local lineno = 0
+ while true do
+ local line=cnf:read('*line')
+ lineno = lineno + 1
+
+ if line == nil then break end -- EOF
+ line = string.gsub(line, '%s*#.*$', '') -- comments begin with #
+ line = string.gsub(line, '%s*$', '') -- remove trailing spaces
+ line = string.gsub(line, '^%s*', '') -- remove leading spaces
+
+ -- try to interpret the line
+ local ok = string.match(line, '^%s*$')
+ or confline_to_alias(line, configfile, lineno)
+ or confline_to_score(line, configfile, lineno)
+ or confline_to_config(line, configfile, lineno)
+
+ -- complain if it failed
+ if not ok then
+ err_print('warning',
+ 'syntax error in %s at line %d.', configfile, lineno)
+ end
+ end
+ cnf:close()
+end
+
+-- return the list of configuration files
+local function get_config_files()
+ -- get names
+ local platform = string.match(kpse.var_value('SELFAUTOLOC'), '.*/(.*)$')
+ local names = {
+ 'texdoc-' .. platform .. '.cnf',
+ 'texdoc.cnf',
+ 'texdoc-dist.cnf',
+ }
+
+ -- get dirs
+ local sep = (os.type == 'windows') and ';' or ':'
+ local texmf_texdoc = kpse.expand_path('$TEXMF/texdoc')
+ local dirs = texmf_texdoc:explode(sep)
+
+ -- merge them
+ local ret = {}
+ for _, dir in ipairs(dirs) do
+ for _, name in ipairs(names) do
+ local pathname = dir .. '/' .. name
+ table.insert(ret, pathname)
+ end
+ end
+ return ret
+end
+
+-- the config_files table is shared by the following two functions
+local setup_config_from_files
+
+do -- begin scope of config_files
+local config_files = {}
+
+-- set config/aliases from all config files
+setup_config_from_files = function()
+ local file_list = get_config_files()
+
+ for i, file in ipairs(file_list) do
+ -- determine the status of the config file
+ local status
+ if lfs.isfile(file) then
+ status = config.lastfile_switch and 'disabled' or 'active'
+ else
+ status = 'not found'
+ end
+
+ -- register it
+ config_files[i] = {
+ path = file,
+ status = status,
+ }
+
+ -- read only if active
+ if status == 'active' then
+ read_config_file(file)
+ end
+ end
+end
+
+-- now a special information function (see -f,--file option)
+function M.show_config_files(is_action)
+ local w32_path = import_function('util', 'w32_path')
+ local dbg_print = import_function('util', 'dbg_print')
+
+ -- controled print_func
+ local indent = is_action and ' ' or ''
+ local print_func = is_action and print
+ or function(s) dbg_print('files', s) end
+
+ -- show the list of configuration files
+ print_func('Configuration files are:')
+ for i, file in ipairs(config_files) do
+ -- if not verbose, do not show "not found" files for -f
+ if file.status ~= "not found"
+ or (not is_action or config.verbosity_level == 3) then
+ print_func(indent .. file.status .. '\t' .. w32_path(file.path))
+ end
+ end
+
+ -- show the recommendation (only for the "files" action)
+ if is_action then
+ print_func('Recommended file(s) for personal settings:')
+ local sep = (os.type == 'windows') and ';' or ':'
+ local texmfhomes = string.explode(kpse.var_value('TEXMFHOME'), sep)
+ for _, home in ipairs(texmfhomes) do
+ print_func(indent .. w32_path(home .. '/texdoc/texdoc.cnf'))
+ end
+ end
+end
+
+end -- end scope of config_files
+
+---------------------- config from locale settings -------------------------
+
+local function setup_config_from_locale()
+ local current = os.setlocale(nil, 'all')
+ os.setlocale('', 'all')
+ local native = os.setlocale(nil, 'time')
+ os.setlocale(current, 'all')
+ local lang = string.match(native, '^[a-z][a-z]')
+ if lang then
+ set_config_element('lang', lang, {src='loc'})
+ end
+end
+
+---------------------- options from built-in defaults ----------------------
+
+-- for default viewer on general Unix, we have a list; the following function is
+-- used to check in the path which program is available
+
+-- check if "name" is the name of a file in the path
+-- Warning: to be used only on Unix! (separators, and PATH irrelevant on win32)
+-- the value of PATH is cached
+local is_in_path
+do local path_list = string.explode(os.getenv('PATH'), ':')
+is_in_path = function(name)
+ for _, path in ipairs(path_list) do
+ if lfs.isfile(path .. '/' .. name) then return true end
+ end
+ return false
+end
+end
+
+-- returns a viewer specific to a desktop environment if relevant
+-- doesn't work on windows (uses io.popen)
+-- logic stolen from xdg-open (http://www.freedesktop.org/) and adapted
+local function desktop_environment_viewer()
+ local xdg_current_desktop = os.getenv('XDG_CURRENT_DESKTOP') or ''
+ if (os.getenv('KDE_SESSION_VERSION') or os.getenv('KDE_FULL_SESSION'))
+ or string.match(xdg_current_desktop, '.*KDE.*') then
+ if is_in_path('kde-open') then return '(kde-open %s) &' end
+ if is_in_path('kfmclient') then return '(kfmclient exec %s) &' end
+ end
+ if os.getenv('GNOME_DESKTOP_SESSION_ID')
+ or string.match(xdg_current_desktop, '.*GNOME.*') then -- gnome
+ if is_in_path('gvfs-open') then return '(gvfs-open %s) &' end
+ if is_in_path('gnome-open') then return '(gnome-open %s) &' end
+ end
+ if not is_in_path('xprop') then return end
+ local xprop_fh = io.popen('xprop -root _DT_SAVE_MODE 2>/dev/null')
+ local xprop_out = xprop_fh:read('*line')
+ xprop_fh:close()
+ if xprop_out and string.find(xprop_out, '= "xfce4"$') then -- xfce
+ return '(exo-open %s) &'
+ end
+end
+
+-- guess a viewer from a list:
+-- - xdg-open from freedesktop if available
+-- - try detecting desktop environments
+-- - or return the first element of "list" whose name is found in path
+-- - or nil
+-- caches results of desktop environment detection
+local guess_viewer
+do local de_viewer
+guess_viewer = function(cmds)
+ -- try the freedesktop method
+ if is_in_path('xdg-open') then
+ return '(xdg-open %s) &'
+ end
+ -- try desktop environment
+ if not de_viewer then de_viewer = desktop_environment_viewer() end
+ if de_viewer then return de_viewer end
+ -- or look along path
+ for _, cmd in ipairs(cmds) do
+ if is_in_path(cmd[1]) then return cmd[2] end
+ end
+end
+end
+
+-- set viewers from defaults (done only if necessary)
+function M.get_default_viewers()
+ local function set_config_ls(ls) set_config_list(ls, {src='def'}) end
+ if (os.type == 'windows') then
+ set_config_ls {
+ -- Use 'start' to get file associations.
+ -- We need to quote the filenames, but the first quoted argument
+ -- is considered as the title by start, so we provide a dummy title.
+ -- Also, since the command line parser removes quotes if there
+ -- is no space inside, the dummy title must contain spaces.
+ viewer_dvi = 'start "texdoc dvi viewer"',
+ viewer_html = 'start "texdoc html viewer"',
+ viewer_pdf = 'start "texdoc pdf viewer"',
+ viewer_ps = 'start "texdoc ps viewer"',
+ -- 'more' is always available.
+ -- However, we can't assume texdoc is called from a cmd.exe window
+ -- (it can be run from the start->run menu), hence we make sure
+ -- to open a new window if needed.
+ viewer_txt = 'start cmd /k more',
+ viewer_md = viewer_txt,
+ }
+ elseif (os.name == 'macosx') then
+ set_config_ls {
+ viewer_dvi = 'open',
+ viewer_html = 'open',
+ viewer_pdf = 'open',
+ viewer_ps = 'open',
+ viewer_txt = 'less',
+ viewer_md = viewer_txt,
+ }
+ else -- generic Unix
+ set_config_ls {
+ viewer_dvi = guess_viewer {
+ {'xdvi', '(xdvi %s) &'},
+ {'evince', '(evince %s) &'},
+ {'okular', '(okular %s) &'},
+ {'kdvi', '(kdvi %s) &'},
+ {'xgdvi', '(xgdvi %s) &'},
+ {'spawg', '(spawg %s) &'},
+ {'spawx11', '(spawx11 %s) &'},
+ {'tkdvi', '(tkdvi %s) &'},
+ {'dvilx', '(dvilx %s) &'},
+ {'advi', '(advi %s) &'},
+ {'xdvik-ja', '(xdvik-ja %s) &'},
+ {'see', '(see %s) &'}
+ },
+ viewer_html = guess_viewer {
+ {'firefox', '(firefox %s) &'},
+ {'seamonkey', '(seamonkey %s) &'},
+ {'mozilla', '(mozilla %s) &'},
+ {'konqueror', '(konqueror %s) &'},
+ {'epiphany', '(epiphany %s) &'},
+ {'opera', '(opera %s) &'},
+ {'w3m', 'w3m'},
+ {'links', 'links'},
+ {'lynx', 'lynx'},
+ {'see', 'see'}
+ },
+ viewer_pdf = guess_viewer {
+ {'xpdf', '(xpdf %s) &'},
+ {'evince', '(evince %s) &'},
+ {'okular', '(okular %s) &'},
+ {'kpdf', '(kpdf %s) &'},
+ {'acroread', '(xpdf %s) &'},
+ {'see', '(see %s) &'}
+ },
+ viewer_ps = guess_viewer {
+ {'gv', '(gv %s) &'},
+ {'evince', '(evince %s) &'},
+ {'okular', '(okular %s) &'},
+ {'kghostview', '(kghostview %s) &'},
+ {'see', '(see %s) &'}
+ },
+ viewer_txt = guess_viewer {
+ {'most', 'most'},
+ {'less', 'less'},
+ {'more', 'more'}
+ },
+ viewer_md = viewer_txt,
+ }
+ end
+end
+
+-- set some fall-back default values if no previous value is set
+local function setup_config_from_defaults()
+ local function set_config_ls(ls) set_config_list(ls, {src='def'}) end
+ local function set_config_elt(key, val)
+ set_config_element(key, val, {src='def'})
+ end
+ -- various, platform independent, stuff
+ set_config_ls {
+ mode = 'view',
+ interact_switch = 'true',
+ machine_switch = 'false',
+ ext_list = 'pdf, htm, html, txt, md, ps, dvi, ',
+ basename_list = 'readme, 00readme',
+ badext_list = 'txt, ',
+ badbasename_list = 'readme, 00readme',
+ suffix_list = '',
+ verbosity_level = C.def_verbosity,
+ debug_list = '',
+ max_lines = '20',
+ fuzzy_level = '5',
+ }
+ -- zip-related options
+ set_config_ls {
+ zipext_list = '',
+ rm_file = 'rm -f',
+ rm_dir = 'rmdir',
+ }
+end
+
+-------------------------- set all configuration ---------------------------
+
+-- populate the config and alias arrays
+function M.setup_config_and_alias(cl_config)
+
+ -- setup config from all sources
+ setup_config_from_cl(cl_config)
+ setup_config_from_env()
+ setup_config_from_files()
+ setup_config_from_locale()
+ setup_config_from_defaults()
+
+ -- machine mode implies no interaction
+ if config.machine_switch == true then
+ real_set_config('interact_switch', false)
+ end
+
+ -- debug implies verbose
+ if #config.debug_list > 0 then
+ real_set_config('verbosity_level', tonumber(C.max_verbosity))
+ end
+
+ -- we were waiting for config.debug_list to be known to do this
+ M.show_config_files()
+end
+
+return M
+
+-- vim: ft=lua:
diff --git a/support/texdoc/script/texdoclib-const.tlu b/support/texdoc/script/texdoclib-const.tlu
new file mode 100644
index 0000000000..5d34bed3f6
--- /dev/null
+++ b/support/texdoc/script/texdoclib-const.tlu
@@ -0,0 +1,158 @@
+-- texdoclib-const.tlu: global constants for texdoc
+--
+-- The TeX Live team, GPLv3, see texdoclib.tlu for details
+
+-- use an empty environment that will become texdoc_env.C (see EOF)
+local constants = {}
+local kpse = kpse
+local setfenv = setfenv
+local texdoc_env
+
+if setfenv then -- Lua <5.2
+ texdoc_env = getfenv()
+ setfenv(1, constants)
+else
+ texdoc_env = _ENV
+ _ENV = constants
+end
+
+-- BEGIN constants
+
+-- progname and version
+fullname = kpse.find_file('texdoc/texdoclib', 'lua')
+progname = 'Texdoc'
+version = '3.1'
+release_date = '2019-03-28'
+
+-- make sure to update setup_config_from_cl() accordingly
+-- and set a default value in setup_config_from_defaults() if relevant
+usage_msg = [[
+Usage: texdoc [OPTION...] NAME...
+ or: texdoc [OPTION...] ACTION
+
+Try to find appropriate TeX documentation for the specified NAME(s).
+Alternatively, perform the given ACTION and exit.
+
+Options:
+ -w, --view Use view mode: start a viewer. (default)
+ -m, --mixed Use mixed mode (view or list).
+ -l, --list Use list mode: show a list of results.
+ -s, --showall Use showall mode: show also "bad" results.
+
+ -i, --interact Use interactive menus. (default)
+ -I, --nointeract Use plain lists, no interaction required.
+ -M, --machine Machine-readable output for lists (implies -I).
+
+ -q, --quiet Suppress warnings and most error messages.
+ -v, --verbose Print additional information (e.g., viewer command).
+ -D, --debug Activate all debug output (equal to "--debug=all").
+ -d LIST, --debug=LIST
+ Activate debug output restricted to LIST.
+ -c NAME=VALUE Set configuration item NAME to VALUE.
+
+Actions:
+ -h, --help Print this help message.
+ -V, --version Print the version number.
+ -f, --files Print the list of configuration files used.
+ --just-view FILE Display FILE, given with full path (no searching).
+
+Full manual available via `texdoc texdoc'.
+
+Website: <https://tug.org/texdoc/>
+Repository: <https://github.com/TeX-Live/texdoc>
+Please email bugs to <texdoc@tug.org>.]]
+
+copyright_msg = [[
+Copyright 2008 Manuel Pégourié-Gonnard, Takuto Asakura, and the TeX Live Team.
+License GPLv3+: GNU GPL version 3 or later <https://gnu.org/licenses/gpl.html>.
+This is free software: you are free to change and redistribute it.]]
+
+actions_ph = 'Actions:\n'
+
+known_options = {
+ 'viewer_.*',
+ 'mode',
+ 'interact_switch',
+ 'machine_switch',
+ 'ext_list',
+ 'basename_list',
+ 'badext_list',
+ 'badbasename_list',
+ 'suffix_list',
+ 'verbosity_level',
+ 'debug_list',
+ 'lastfile_switch',
+ 'rm_dir',
+ 'rm_file',
+ 'unzip_.*',
+ 'zipext_list',
+ 'max_lines',
+ 'lang',
+ 'fuzzy_level',
+ 'texlive_tlpdb',
+}
+
+error_msg = [[
+Try `texdoc --help' for short help, `texdoc texdoc' for full manual.]]
+
+notfound_msg = [[
+Sorry, no documentation found for "PKGNAME".
+If you are unsure about the name, try full-text searching on CTAN.
+Search form: <https://www.ctan.org/search/>]]
+notfound_msg_ph = 'PKGNAME'
+
+-- exit codes
+exit_ok = 0
+exit_error = 1 -- apparently hard-coded in Lua
+exit_usage = 2
+exit_notfound = 3
+
+err_priority = {
+ error = 1,
+ warning = 2,
+ info = 3,
+}
+min_verbosity = '0' -- (nothing at all)
+max_verbosity = '3'
+def_verbosity = '2'
+
+known_debugs = {
+ config = {'files'},
+ files = {},
+ search = {},
+ score = {},
+ texdocs = {},
+ tlpdb = {},
+ version = {},
+ view = {},
+}
+
+-- various cache or non-cache files
+cache_name = 'texdoc/cache-tlpdb.lua' -- relative to TEXMFVAR
+data_tlpdb_name = 'texdoc/Data.tlpdb.lua'
+
+place_holder = '%%s' -- used for viewer commands
+
+-- END constants
+
+-- get our previous environment back
+if setfenv then
+ setfenv(1, texdoc_env)
+else
+ _ENV = texdoc_env
+end
+
+-- Make table C a read-only proxy to the local <constants>.
+-- Note this is not deep read-only: C.known_debugs is read-only, but
+-- C.known_debugs.version isn't, for instance.
+local C = {}
+setmetatable(C, {
+ __index = constants,
+ __newindew = function()
+ error('Internal error: attempt to modify a constant.')
+ end
+})
+
+return C
+
+-- vim: ft=lua:
diff --git a/support/texdoc/script/texdoclib-score.tlu b/support/texdoc/script/texdoclib-score.tlu
new file mode 100644
index 0000000000..81e1be2a33
--- /dev/null
+++ b/support/texdoc/script/texdoclib-score.tlu
@@ -0,0 +1,293 @@
+-- texdoclib-score.tlu: scoring functions for texdoc
+--
+-- The TeX Live Team, GPLv3, see texdoclib.tlu for details
+
+-- dependencies
+local texdoc = {
+ util = require('texdoclib-util'),
+ config = require('texdoclib-config'),
+}
+
+-- shortcuts
+local M = {}
+local dbg_print = texdoc.util.dbg_print
+
+-- shared variables
+local global_adjscore, spec_adjscore = {}, {}
+
+------------------------- configuration directives -------------------------
+
+-- set key in score table to val, without overriding
+local function set_score_table(tab, key, val)
+ local k = string.lower(key)
+ local v = tonumber(val)
+ if v then
+ if tab[k] == nil then tab[k] = v end
+ return true
+ end
+ return false
+end
+
+-- interpret a confline as a score directive or return false
+function M.confline_to_score(line, file, pos)
+ local keyw, pat, val
+ -- try global adjscore
+ pat, val = string.match(line, '^adjscore%s+([%w%p]+)%s*=%s*([%d+-.]+)')
+ if pat and val then
+ return set_score_table(global_adjscore, pat, val)
+ end
+ -- try keyword specific adjscore
+ keyw, pat, val = string.match(line,
+ '^adjscore%(([%w%p]+)%)%s+([%w%p]+)%s*=%s*([%d+-.]+)')
+ if keyw and pat and val then
+ keyw = string.lower(keyw)
+ spec_adjscore[keyw] = spec_adjscore[keyw] or {}
+ return set_score_table(spec_adjscore[keyw], pat, val)
+ end
+ return false
+end
+
+---------------------------- score computation -----------------------------
+
+-- says if pat is a "subword" of str
+local function is_subword(str, pat)
+ local function is_delim(str, i)
+ return not not string.find(string.sub(str, i, i), '%p')
+ end
+
+ local i, j = string.find(str, pat, 1, true)
+ return not not (i and j
+ and (i == 1 or is_delim(str, i) or is_delim(str, i-1))
+ and (j == #str or is_delim(str, j) or is_delim(str, j+1)))
+end
+
+-- says if a filename has a bad basename
+local function has_bad_basename(file)
+ file = file:gsub('.*/', '')
+ for _, b in ipairs(texdoc.config.get_value('badbasename_list')) do
+ if file:find('^' .. b .. '$') or file:find('^' .. b .. '%.') then
+ return true
+ end
+ end
+ return false
+end
+
+-- compute a heuristic score -10 <= s < 10
+local function heuristic_score(file, pat)
+ dbg_print('score', 'Start heuristic scoring with pattern: ' .. pat)
+ -- score management
+ local score = -10
+ local function upscore(s, reason, force)
+ if s > score or force then
+ score = s
+ dbg_print('score',
+ 'New heuristic score: %.1f. Reason: %s', s, reason)
+ end
+ end
+ local slash = not not string.find(pat, '/', 1, true)
+ -- look for exact or subword match
+ if M.is_exact_locale(file, pat) then
+ upscore(5, 'exact match with correct locale')
+ elseif M.is_exact(file, pat) then
+ upscore(4, 'exact match')
+ elseif is_subword(file, pat) then
+ upscore(1, 'subword match')
+ end
+ -- try derivatives unless pat contains a slash
+ if not slash then
+ for _, suffix in ipairs(texdoc.config.get_value('suffix_list')) do
+ local deriv = pat..suffix
+ if M.is_exact(file, deriv) then
+ upscore(3, 'exact match for derived pattern: ' .. deriv)
+ elseif is_subword(file, deriv) then
+ upscore(2, 'subword match for derived pattern: ' .. deriv)
+ end
+ end
+ end
+ -- if extension is bad, score becomes an epsilon
+ local ext = texdoc.config.get_value('ext_list')[M.ext_pos(file)]
+ if ext and texdoc.config.get_value('badext_list_inv')[ext] and score > 0 then
+ upscore(0.1, 'bad extension', true)
+ end
+ -- if basename is bad, score becomes an epsilon
+ if has_bad_basename(file) and score > 0 then
+ upscore(0.1, 'bad basename', true)
+ end
+ -- bonus for being in the right directory
+ if string.find('/' .. file, '/' .. pat .. '/', 1, true) and not slash then
+ upscore(score + 1.5, 'directory bonus')
+ end
+ -- done
+ dbg_print('score', 'Final heuristic score: %.1f', score)
+ return score
+end
+
+-- set the score of a docfile
+local function set_score(df, original_kw)
+ -- scoring is case-insensitive (patterns are already lowercased)
+ local name = string.lower(df.shortname)
+ dbg_print('score', '----------')
+ dbg_print('score', 'Start scoring ' .. df.realpath)
+ dbg_print('score', 'Name used: ' .. name)
+ -- get score from patterns
+ local score = -10
+ for _, pat in ipairs(df.matches) do
+ local s = -10
+ local p = string.lower(pat.name)
+ if pat.original then
+ s = df.tree > -1 and heuristic_score(name, p) or 1
+ elseif M.is_exact(name, p) then
+ local bonus, note = 0, ''
+ if pat.locale then
+ bonus, note = 5, ', (language-based)'
+ end
+ s = (pat.score or 10) + bonus -- default alias score is 10
+ dbg_print('score',
+ 'Matching alias "%s", score: %.1f%s', pat.name, s, note)
+ end
+ if s > score then score = s end
+ end
+ dbg_print('score', 'Max pattern score: %.1f', score)
+ -- get score from tlp associations
+ if score == -10 and df.tlptodoc then
+ score = -1
+ dbg_print('score',
+ 'New score: %.1f from package name association', score)
+ end
+ if score == -10 and df.runtodoc then
+ score = -5
+ dbg_print('score',
+ 'New score: %.1f from sty/cls association', score)
+ end
+ -- bonus for metadata
+ if df.details then
+ if string.find(string.lower(df.details), 'readme') then
+ score = score + 0.1
+ dbg_print('score', 'Catalogue "readme" bonus: +0.1')
+ else
+ score = score + 1.5
+ dbg_print('score', 'Catalogue details bonus: +1.5')
+ end
+ end
+ -- adjust from keyword-specific tables
+ if df.tree > -1 and spec_adjscore[original_kw] then
+ for pat, val in pairs(spec_adjscore[original_kw]) do
+ if val and is_subword('/' .. name, pat) then
+ score = score + val
+ dbg_print('score',
+ 'Adjust by %.1f from specific pattern "%s"', val, pat)
+ end
+ end
+ end
+ -- adjust from global tables
+ if df.tree > -1 then
+ for pat, val in pairs(global_adjscore) do
+ if val and is_subword('/' .. name, pat) then
+ if score > -10 or val < 0 then score = score + val end
+ dbg_print('score',
+ 'Adjust by %.1f from global pattern "%s"', val, pat)
+ end
+ end
+ end
+ dbg_print('score', 'Final score: %.1f', score)
+ df.score = score
+end
+
+-- set the scores for a doclist
+local function set_list_scores(list, original_kw)
+ for _, df in ipairs(list) do
+ set_score(df, original_kw)
+ end
+end
+
+-- says if file is an exact match for pat
+function M.is_exact(file, pat)
+ file = texdoc.util.parse_zip(file)
+ local slashes = string.gsub(pat, '[^/]+', '[^/]+')
+ basename = string.match(file, slashes .. '$')
+ if not basename then return nil end
+ if basename == pat then return true end
+ for _, ext in ipairs(texdoc.config.get_value('ext_list')) do
+ if ext ~= '' and ext ~= '*' and basename == pat .. '.' .. ext then
+ return true
+ end
+ end
+ return false
+end
+
+-- says if file is an exact match for pat and the current locale
+function M.is_exact_locale(file, pat)
+ if string.match(pat, '%-%l%l%l?$') then
+ -- don't match if the pattern appears to include a language code
+ return false
+ end
+ local lang = texdoc.config.get_value('lang')
+ if lang then
+ return M.is_exact(file, pat .. '-' .. lang)
+ or M.is_exact(file, lang .. '-' .. pat)
+ end
+ return false
+end
+
+-- compare two docfile's: (see texdoclib-search.tlu for structure)
+-- 1. by score
+-- 2. then by extensions (ordered as in ext_list),
+-- 3. then lexicographically by fullpath.
+-- 4. then by tree.
+-- return true if a is better than b
+local function docfile_order(a, b)
+ if a.score > b.score then return true
+ elseif a.score < b.score then return false
+ elseif a.ext_pos < b.ext_pos then return true
+ elseif a.ext_pos > b.ext_pos then return false
+ elseif a.realpath < b.realpath then return true
+ elseif a.realpath > b.realpath then return false
+ else return (a.tree > b.tree)
+ end
+end
+
+----------------------------- public functions -----------------------------
+
+-- returns the index of the most specific extension of file in ext_list,
+-- or config.ext_list_max + 1
+function M.ext_pos(file)
+ -- remove zipext if applicable
+ file = texdoc.util.parse_zip(file)
+ -- now find the extension
+ local p, e, pos, ext
+ for p, e in ipairs(texdoc.config.get_value('ext_list')) do
+ if (e == '*') and (ext == nil) then
+ pos, ext = p, e
+ elseif (e == '') and not string.find(file, '.', 1, true) then
+ pos, ext = p, e
+ elseif string.sub(file, -string.len(e)-1) == '.' .. e then
+ if (ext == nil) or (ext == '*')
+ or (string.len(e) > string.len(ext)) then
+ pos, ext = p, e
+ end
+ end
+ end
+ return pos or (texdoc.config.get_value('ext_list_max') + 1)
+end
+
+-- return the "quality" of docfile
+function M.docfile_quality(df)
+ if df.score > 0 then
+ return 'good'
+ elseif df.score > -100 then
+ return 'bad'
+ else
+ return 'killed'
+ end
+end
+
+-- sort a doclist
+function M.sort_doclist(dl, original_kw)
+ dl:stop()
+ set_list_scores(dl, original_kw)
+ table.sort(dl, docfile_order)
+end
+
+return M
+
+-- vim: ft=lua:
diff --git a/support/texdoc/script/texdoclib-search.tlu b/support/texdoc/script/texdoclib-search.tlu
new file mode 100644
index 0000000000..1a7270d0b7
--- /dev/null
+++ b/support/texdoc/script/texdoclib-search.tlu
@@ -0,0 +1,771 @@
+-- texdoclib-search.tlu: file searching functions for texdoc
+--
+-- The TeX Live Team, GPLv3, see texdoclib.tlu for details
+
+-- Warning: Some functions here assume that M.init_databases() has been called.
+
+-- dependencies
+local texdoc = {
+ const = require('texdoclib-const'),
+ util = require('texdoclib-util'),
+ alias = require('texdoclib-alias'),
+ score = require('texdoclib-score'),
+ config = require('texdoclib-config'),
+}
+
+-- shortcuts
+local M = {}
+local C = texdoc.const
+local err_print = texdoc.util.err_print
+local dbg_print = texdoc.util.dbg_print
+
+-- shared by all functions in this file
+local s_doclist -- the Doclist object to be populated by various functions
+local s_meta -- {[normname] = meta, ...} (populated by init_tlp_database)
+local vanilla -- is this a vanilla TL or a re-package one without tlpdb?
+
+---------------------------- utility functions -----------------------------
+
+-- find the TeX Live root
+local function get_tlroot()
+ local tlroot = kpse.var_value('TEXMFROOT')
+ get_tlroot = function() return tlroot end
+ return tlroot
+end
+
+-- says if file has a known extension according to ext_list
+-- (or known basename according to basename_list)
+local function check_ext(file)
+ file = string.lower(file)
+ -- remove zipext if applicable
+ file = texdoc.util.parse_zip(file)
+ -- then do the normal thing
+ for _, e in ipairs(texdoc.config.get_value('ext_list')) do
+ if e == '*' then
+ return true
+ elseif (e == '') then
+ if not string.find(file, '.', 1, true) then
+ return true
+ end
+ else
+ local dot_e = '.'..e
+ if string.sub(file, -string.len(dot_e)) == dot_e then
+ return true
+ end
+ end
+ end
+ -- is the basename good?
+ for _, b in ipairs(texdoc.config.get_value('basename_list')) do
+ if file:find('^' .. b .. '$') or file:find('^' .. b .. '%.') then
+ return true
+ end
+ end
+ return false
+end
+
+----------------------- docfile and doclist objects ------------------------
+
+--[[
+doclist = {
+ [1] = docfile1, [2] = docfiles2, ...,
+ inv = {realpath1 = index1, ...}
+}
+
+The inv subtable is such that for all i
+doclist.inv(doclist[i].realpath:lower()) == i
+Paths are lowercased in order to avoid duplicates on windows.
+--]]
+
+local Doclist = {}
+Doclist.__index = Doclist
+
+-- create a new list of docfiles
+function Doclist:new()
+ local dl = {inv = {}}
+ setmetatable(dl, self)
+ return dl
+end
+
+-- add a docfile to a list
+function Doclist:add(df)
+ -- if no realpath information, unable to add
+ -- (useful if vanilla == false)
+ if not df.realpath then return end
+
+ -- check the existence of the file
+ if not lfs.isfile(df.realpath) then
+ dbg_print('search', 'File %s not found. Skipping.', df.realpath)
+ return
+ end
+
+ -- add the docfile to the list
+ local index = self.inv[df.realpath:lower()]
+ if index then
+ self[index]:mergein(df)
+ else
+ dbg_print('search', 'File %s found.', df.realpath)
+
+ local newindex = #self + 1
+ self[newindex] = df
+ self.inv[df.realpath:lower()] = newindex
+ end
+end
+
+-- stops a doclist
+function Doclist:stop()
+ self.inv = nil
+end
+
+--[[
+docfile = {
+ -- name and tree are mandatory
+ name = filename (used for scoring only)
+ tree = code of the tree, see below
+ -- at least one of the following fields should exist
+ matches = {pattern1, pattern2, ...} or {}
+ runtodoc = true if there is a runfile -> docfile association
+ tlptodoc = true if there is a tlp name -> docfile association
+ -- those are virtual members, see below
+ realpath = full path
+ normname = nomrmalised (path removed up to the 'doc' component)
+ shortname = short name used for scoring
+ basename = basename
+ lang = language tag from the catalogue metadata
+ details = details tag from the catalogue metadata
+ quality = 'good', 'bad', or 'killed' depending on score
+ ext_pos = position of the extension in ext_list
+ -- set for elements of a list as a side effect of sort_doclist()
+ score = score
+}
+if tree > 1, this is the index of the tree in TEXDOCS
+if tree = 0, then name is relative to TLROOT
+tree = - 1 if and only if file is a sty file. Here name is absolute.
+--]]
+
+-- Docfile objects inherit members from Docfile
+-- and have virtual members implemented by get_<member>() methods
+-- for best cache performance, getters should not return nil
+local Docfile = {}
+function Docfile:__index(key)
+ if Docfile[key] then return Docfile[key] end
+ local getter = Docfile['get_' .. key]
+ if getter then
+ rawset(self, key, getter(self))
+ return rawget(self, key)
+ end
+end
+
+-- create a new docfile object using initialisation info
+-- required fields: name, tree
+function Docfile:new(info)
+ local df = {}
+ setmetatable(df, self)
+ for k, v in pairs(info) do
+ if k == 'pattern' then
+ df.matches = {info.pattern}
+ else
+ df[k] = v
+ end
+ end
+ return df
+end
+
+-- merge a second docfile object in, assuming it represents the same file
+function Docfile:mergein(df)
+ for k, v in pairs(df) do
+ if k == 'matches' then
+ for _, m in ipairs(df.matches or {}) do
+ table.insert(self.matches, m)
+ end
+ else
+ self[k] = v
+ end
+ end
+end
+
+-- return the full path to the file
+local texdocs_tree_to_path -- definition later
+function Docfile:get_realpath()
+ if self.tree > 0 then
+ return texdocs_tree_to_path(self.tree, self.name)
+ elseif self.tree == 0 then
+ if vanilla then
+ return get_tlroot() .. '/' .. self.name
+ else
+ return kpse.find_file(self.normname, 'TeX system documentation')
+ end
+ else
+ return self.name
+ end
+end
+
+-- normalise a name from the tlpdb (use for s_meta indexes)
+local function reloc_tlpdb_path(name)
+ return string.gsub(name, '^texmf[^/]*/doc/', '', 1)
+end
+
+-- return normalised name
+function Docfile:get_normname()
+ return (self.tree == 0) and reloc_tlpdb_path(self.name) or self.name
+end
+
+-- retrieve the lang from meta
+function Docfile:get_lang()
+ local meta = s_meta[self.normname]
+ return meta and (meta.lang or false) or false
+end
+
+-- retrieve the details from meta
+function Docfile:get_details()
+ local meta = s_meta[self.normname]
+ return meta and (meta.details or false) or false
+end
+
+-- return the short name used for scoring
+function Docfile:get_shortname()
+ if self.tree == -1 then return self.name end
+ -- remove first component of name if at least two directory levels
+ return string.match(self.normname, '^..-/(.+/.+)$') or self.normname
+end
+
+-- return the base name
+function Docfile:get_basename()
+ return string.gsub(self.name, '.*/', '', 1)
+end
+
+-- for interface consistency, matches should always be a table, never nil
+function Docfile:get_matches()
+ return {}
+end
+
+-- from texdoclib-score.tlu
+Docfile.get_quality = texdoc.score.docfile_quality
+
+-- from texdoclib-score.tlu
+function Docfile:get_ext_pos()
+ return texdoc.score.ext_pos(self.basename)
+end
+
+-------------------- select results from TEXDOCS trees ---------------------
+
+-- says if a file (with its path) matches a pattern
+local function matches(pattern, file)
+ if pattern.original then
+ return string.find(file:lower(), pattern.name:lower(), 1, true)
+ else
+ return texdoc.score.is_exact(file, pattern.name)
+ end
+end
+
+-- return a docfile object if file "matches", nil otherwise
+local function process_file(patlist, file, pathfile, code)
+ local docfile
+ local pattern
+ for _, pattern in ipairs(patlist) do
+ if matches(pattern, pathfile) then
+ local info = {
+ name = pathfile,
+ tree = code,
+ pattern = pattern,
+ }
+ if docfile then
+ docfile:mergein(Docfile:new(info))
+ else
+ docfile = Docfile:new(info)
+ end
+ end
+ end
+ return docfile
+end
+
+-- scan a database
+local function scan_db(patlist, code, lsr_db)
+ for file, basename in pairs(lsr_db) do
+ local df = process_file(patlist, basename, file, code)
+ if df then s_doclist:add(df) end
+ end
+end
+
+--------------------- manage TEXDOCS trees à la kpse ----------------------
+
+-- build a db from a ls-R file
+local function init_lsr_db(root, shift)
+ -- open the file
+ local lsr = assert(io.open(root .. '/ls-R', 'r'))
+ local _ = lsr:read('*line') -- throw away first line (comment)
+
+ -- scan it
+ local db = {}
+ local maybe_dir, isdoc = true, false
+ local current_dir
+ local l = #shift
+ while true do
+ local line = lsr:read('*line')
+ while line == '' do line, maybe_dir = lsr:read('*line'), true end
+ if line == nil then break end -- EOF
+ local dir_line = maybe_dir and string.match(line, '^%./(.*):$')
+ if dir_line then
+ maybe_dir = false -- next line may not be a dir
+ if string.sub(dir_line .. '/', 1, l) == shift then
+ isdoc = true
+ current_dir = string.sub(dir_line, l+1)
+ db[current_dir] = nil
+ elseif isdoc then
+ break -- we're exiting the ./doc (or shift) dir, so it's over
+ end
+ elseif isdoc then
+ local file = (current_dir == '') and line or current_dir .. '/' .. line
+ if check_ext(line) then db[file] = line end
+ end
+ end
+
+ lsr:close()
+
+ return db
+end
+
+-- build a db for a tree without ls-R index
+local function init_tree_db(base, recurse)
+ local db = {}
+ local function init_tree_db_rec(dir)
+ for file in lfs.dir(base .. '/' .. dir) do
+ if file ~= '.' and file ~= '..' then
+ local f = (dir == '') and file or dir .. '/' .. file
+ if lfs.isdir(base..'/'..f) then
+ if recurse then init_tree_db_rec(f) end
+ else
+ if check_ext(file) then db[f] = file end
+ end
+ end
+ end
+ end
+ init_tree_db_rec('')
+ return db
+end
+
+local init_texdocs_database, get_doclist_texdocs
+
+do -- begin scope of doc_roots
+local doc_roots
+
+--[[
+doc_roots[i] = {
+ path = initial path,
+ db = {[file1] = basename1, [file2] = basename2, ...},
+}
+--]]
+
+-- populate the doc_roots filename databases
+init_texdocs_database = function()
+ -- find a ls-R file in a parent directory and return it or nil
+ local function lsr_root(path)
+ if not lfs.isdir (path) then return end
+ local root, shift = path, ''
+ if string.sub(root, -1) == '/' then root = string.sub(root, 1, -2) end
+ while string.find(root, '/', 1, true) do
+ if lfs.isfile(root .. '/ls-R') then
+ return root, shift
+ end
+ local last_comp = string.match(root, '^.*/(.*)$')
+ -- /!\ cannot put last_comp in a regex: can contain special char
+ root = string.sub(root, 1, - (#last_comp + 2))
+ shift = last_comp .. '/' .. shift
+ end
+ end
+
+ doc_roots = {}
+ local sep = (os.type == 'windows') and ';' or ':'
+ local kpse_texdocs = kpse.expand_var("$TEXDOCS")
+
+ -- expand the path and turn it into a lua list
+ local raw_doc_roots = string.explode(kpse.expand_braces(kpse_texdocs), sep)
+ local max = #raw_doc_roots + 1
+
+ for j, dir in ipairs(raw_doc_roots) do
+ local i = max - j
+ local n
+ local path, db
+
+ -- get path, !! and // values
+ dir, n = string.gsub (dir, '//$', '')
+ local recursion_allowed = (n == 1)
+ local path, n = string.gsub (dir, '^!!', '')
+ local index_mandatory = (n == 1)
+ dbg_print('texdocs',
+ 'texdocs[%d] = %s (index_mandatory=%s, recursion_allowed=%s)',
+ i, path, tostring(index_mandatory), tostring(recursion_allowed))
+
+ -- decide if we should use a ls-R index, the filesystem, or do nothing
+ local root, shift = lsr_root(path)
+ if root and shift and recursion_allowed then
+ dbg_print('texdocs',
+ 'texdocs[%d] using index: %s (shift=%s)', i, root, shift)
+ db = init_lsr_db(root, shift)
+ elseif not index_mandatory and lfs.isdir(path) then
+ dbg_print('texdocs',
+ 'texdocs[%d] using filesystem search', i)
+ db = init_tree_db(path, recursion_allowed)
+ end
+
+ -- register this in docroots
+ doc_roots[i] = {path=path, db=db}
+ end
+end
+
+-- find docfiles in texdocs directories
+get_doclist_texdocs = function(patlist)
+ for code, dr in ipairs(doc_roots) do
+ if dr.db then scan_db(patlist, code, dr.db) end
+ end
+end
+
+-- return the real path from a texdocs tree number + relative path
+texdocs_tree_to_path = function (tree, rel)
+ return doc_roots[tree].path .. '/' .. rel
+end
+
+end -- end scope of doc_roots
+
+---------------------------- look for sty files ----------------------------
+
+-- add doclist entries for sty files in patlist
+local function get_doclist_sty(patlist)
+ for _, pat in ipairs(patlist) do
+ local file = kpse.find_file(pat.name)
+ if file then
+ local df = Docfile:new({
+ name = file,
+ tree = -1,
+ pattern = pat,
+ })
+ s_doclist:add(df)
+ end
+ end
+end
+
+-------------------------------- use tlpdb ---------------------------------
+
+-- tlpdb mean TeX Live Package DataBase and tlp means TeX Live Package
+
+-- return true if cache exists and is newer than original, false otherwise
+local function good_cache(cache, ori)
+ local cache_date = lfs.attributes(cache, 'modification')
+ if not cache_date then return false end
+ local ori_date = assert(lfs.attributes(ori, 'modification'))
+ return cache_date > ori_date
+end
+
+-- make sure a given directory exists, or return nil plus an error string
+local function mkdir_p(dir)
+ if lfs.isdir(dir) then return true end
+ local parent = texdoc.util.path_parent(dir)
+ if parent then
+ local ok, msg = mkdir_p(parent)
+ if not ok then return nil, msg end
+ end
+ return lfs.mkdir(dir)
+end
+
+local print_out_tlpinfo, get_doclist_tlpdb
+local get_tlpinfo_from_tlpdb, get_tlpinfo_from_cache, get_tlpinfo_from_dist
+
+do -- begin scope of tlpinfo tables
+local tlp_from_runfile -- {[runfile_basename] = {tlp1 = true, ...}, ...}
+local tlp_doclist -- {[tlp_name] = {relname1, relname2, ...}, ...}
+
+-- remove entries for tlp without any docfile
+local function remove_useless_tlp()
+ for tlp, doclist in pairs(tlp_doclist) do
+ if #doclist == 0 then tlp_doclist[tlp] = nil end
+ end
+ for runfile, tlp_set in pairs(tlp_from_runfile) do
+ for tlp in pairs(tlp_set) do
+ if not tlp_doclist[tlp] then
+ tlp_from_runfile[runfile][tlp] = nil
+ end
+ end
+ end
+end
+
+-- populate tlpinfo tables using the given texlive.tlpdb
+get_tlpinfo_from_tlpdb = function(filename)
+ s_meta, tlp_from_runfile, tlp_doclist = {}, {}, {}
+ local curr_tlp
+ local state = 'none'
+ for line in io.lines(filename) do
+ if state == 'none' and string.find(line, '^name ') then
+ -- begin a new package
+ curr_tlp = string.lower(string.sub(line, 6, -1))
+ tlp_doclist[curr_tlp] = {}
+ elseif state == 'docfiles' then
+ if not string.find(line, '^ ') then
+ state = 'none'
+ else
+ local file = string.match(line, '^ ([^ ]*)')
+ local meta = string.match(line, '^ [^ ]* (.+)')
+ local basename = string.match(file, '([^/]*)$')
+ if check_ext(basename) then
+ -- we've got a docfile here, add it
+ table.insert(tlp_doclist[curr_tlp], file)
+ if meta then
+ local details = string.match(meta, 'details="([^"]+)"')
+ local lang = string.match(meta, 'language="([^"]+)"')
+ s_meta[reloc_tlpdb_path(file)] = {
+ details = details,
+ lang = lang,
+ }
+ end
+ end
+ end
+ elseif state == 'runfiles' then
+ if not string.find(line, '^ ') then
+ state = 'none'
+ else
+ -- check for interesting runfiles
+ local e = string.sub(line, -4, -1)
+ if e == '.tex' or e == '.sty' or e == '.cls' then
+ local f = string.match(line, '.*/(.*)%.')
+ tlp_from_runfile[f] = tlp_from_runfile[f] or {}
+ tlp_from_runfile[f][curr_tlp] = true
+ end
+ end
+ end
+ -- update state
+ if string.find(line, '^docfiles ') then
+ state = 'docfiles'
+ elseif string.find(line, '^runfiles ') then
+ state = 'runfiles'
+ end
+ end
+ remove_useless_tlp()
+end
+
+-- print out data from tlpdb in dofile()-able form
+print_out_tlpinfo = function(filename)
+ local fh = assert(io.open(filename, 'w'))
+ local function printf(s, ...) fh:write(string.format(s, ...)) end
+
+ -- s_meta
+ printf('local s_meta = {\n')
+ for k, v in pairs(s_meta) do
+ printf(' [%q] = {', k)
+ for i, j in pairs(v) do printf('[%q] = %q, ', i, j) end
+ printf('},\n')
+ end
+ printf('}\n')
+
+ -- tlp_from_runfile
+ printf('local tlp_from_runfile = {\n')
+ for k, v in pairs(tlp_from_runfile) do
+ printf(' [%q] = {', k)
+ for f in pairs(v) do printf('[%q]=true,', f) end
+ printf('},\n')
+ end
+ printf('}\n')
+
+ -- tlp_doclist
+ printf('local tlp_doclist = {\n')
+ for k, v in pairs(tlp_doclist) do
+ printf(' [%q] = {\n', k)
+ for _, f in ipairs(v) do printf(' %q,\n', f) end
+ printf(' },\n')
+ end
+ printf('}\n')
+ printf('return s_meta, tlp_from_runfile, tlp_doclist\n')
+
+ fh:close()
+end
+
+-- get pre-hashed tlpdb info from a cache file
+get_tlpinfo_from_cache = function(filename)
+ s_meta, tlp_from_runfile, tlp_doclist = dofile(filename)
+end
+
+-- get pre-hashed tlpdb info from a pseudo-cache file
+get_tlpinfo_from_dist = function()
+ local f = kpse.find_file(C.data_tlpdb_name, 'texmfscripts')
+ if not f then
+ err_print('error', 'No texlive.tlpdb nor shipped tlpdb data found.')
+ os.exit(C.exit_usage)
+ end
+ dbg_print('tlpdb', 'Getting data from shipped tlpdb data file ' .. f)
+ s_meta, tlp_from_runfile, tlp_doclist = dofile(f)
+end
+
+-- get docfiles for pattern using specific tlpdb information
+get_doclist_tlpdb = function(pattern)
+ -- runfile to tlp to docfile
+ if tlp_from_runfile[pattern] then
+ for tlp in pairs(tlp_from_runfile[pattern]) do
+ for _, file in ipairs(tlp_doclist[tlp]) do
+ s_doclist:add(Docfile:new{
+ name = file,
+ tree = 0,
+ runtodoc = true,
+ })
+ end
+ end
+ end
+ -- tlp name to docfile
+ if tlp_doclist[pattern] then
+ for _, file in ipairs(tlp_doclist[pattern]) do
+ s_doclist:add(Docfile:new{
+ name = file,
+ tree = 0,
+ tlptodoc = true,
+ })
+ end
+ end
+end
+
+-- calculating Levenshtein distance by dynamic programming
+-- cf. http://lua-users.org/lists/lua-l/2008-01/msg00095.html
+function M.levenshtein(s, t)
+ local s, t = tostring(s), tostring(t)
+ if type(s) == 'string' and type(t) == 'string' then
+ local m, n, d = #s, #t, {}
+ for i = 0, m do d[i] = {[0] = i} end
+ for j = 1, n do d[0][j] = j end
+ for i = 1, m do
+ for j = 1, n do
+ local cost = s:sub(i,i) == t:sub(j,j) and 0 or 1
+ d[i][j] = math.min(
+ d[i-1][j] + 1, d[i][j-1] + 1, d[i-1][j-1] + cost
+ )
+ end
+ end
+ return d[m][n]
+ end
+end
+
+-- fuzzy search by using Levenshtein distance
+function M.fuzzy_search(pattern)
+ local tmp_d
+ local min = math.huge
+ local result = ''
+
+ for p in pairs(tlp_doclist) do
+ tmp_d = M.levenshtein(pattern, p)
+ if tmp_d < min then
+ min, result = tmp_d, p
+ end
+ end
+
+ return result, min
+end
+
+end -- end scope of tlpinfo table
+
+-- get tlpinfo tables initialised by whatever mean
+local function init_tlp_database()
+ local TEXMFVAR = kpse.var_value('TEXMFVAR')
+ local cache_file = TEXMFVAR .. '/' .. C.cache_name
+
+ -- set vanilla and detect texlive.tlpdb to use
+ local texlive_tlpdb = get_tlroot() .. '/tlpkg/texlive.tlpdb'
+ vanilla = lfs.isfile(texlive_tlpdb)
+ local tlpdb_found = vanilla
+
+ local custom_texlive_tlpdb = texdoc.config.get_value('texlive_tlpdb')
+ if custom_texlive_tlpdb then
+ if lfs.isfile(custom_texlive_tlpdb) then
+ texlive_tlpdb = custom_texlive_tlpdb
+ tlpdb_found = true
+ else
+ err_print('warning',
+ 'Specified texlive.tlpdb does not exist: ' ..
+ texdoc.util.w32_path(custom_texlive_tlpdb))
+ err_print('warning',
+ 'Fallback to use the texlive.tlpdb in the distribution.')
+ end
+ end
+
+ -- get tlpinfo
+ if tlpdb_found then
+ if good_cache(cache_file, texlive_tlpdb) then
+ dbg_print('tlpdb', 'Using cached data from ' .. cache_file)
+ get_tlpinfo_from_cache(cache_file)
+ else
+ dbg_print('tlpdb', 'Getting data from tlpdb file ' .. texlive_tlpdb)
+ get_tlpinfo_from_tlpdb(texlive_tlpdb)
+ dbg_print('tlpdb', 'Writing data in cache file ' .. cache_file)
+ local ok, msg = mkdir_p(texdoc.util.path_parent(cache_file))
+ if not ok then
+ err_print('warning',
+ 'Failed to create cache file in %s:', cache_file)
+ err_print('warning', msg)
+ else
+ print_out_tlpinfo(cache_file)
+ end
+ end
+ else
+ dbg_print('tlpdb', 'Using shipped tlpdb data.')
+ get_tlpinfo_from_dist()
+ end
+end
+
+------------------------------ main function -------------------------------
+
+-- initialise the various databases (must be called first)
+function M.init_databases()
+ init_texdocs_database()
+ init_tlp_database()
+end
+
+-- find docfiles according to pattern
+function M.get_doclist(pattern, no_alias)
+ -- separate sty patterns from the rest
+ local function normal_vs_sty(list)
+ local normal, sty = {}, {}
+ for _, p in ipairs(list) do
+ if string.match(string.lower(p.name), '%.([^/.]*)$') == 'sty' then
+ table.insert(sty, p)
+ else
+ table.insert(normal, p)
+ end
+ end
+ return normal, sty
+ end
+
+ local function doc_search(pattern, no_alias)
+ -- get patterns (inc. aliases)
+ local patterns = texdoc.alias.get_patterns(pattern, no_alias)
+ local normal, sty = normal_vs_sty(patterns)
+
+ -- get results; _texdocs search comes after _tlpdb search so that
+ -- files found by both will have the priority of the _texdocs tree.
+ -- (https://puszcza.gnu.org.ua/bugs/?369)
+ get_doclist_sty(sty)
+ get_doclist_tlpdb(pattern)
+ get_doclist_texdocs(normal)
+ end
+
+ -- initialise result list
+ s_doclist = Doclist:new()
+
+ -- 1. normal search with the input pattern
+ doc_search(pattern, no_alias)
+
+ -- 2. if no result, execute fuzzy search
+ local fuzzy_level = texdoc.config.get_value('fuzzy_level')
+ if not s_doclist[1] and fuzzy_level > 0 then
+ local f_res, f_lev = M.fuzzy_search(pattern)
+ if f_lev <= fuzzy_level then
+ err_print('info', 'Fuzzy search result: ' .. f_res)
+ dbg_print('search', 'Levenshtein distance: ' .. f_lev)
+ pattern = f_res
+ doc_search(pattern, no_alias)
+ else
+ dbg_print('search', 'Fuzzy search result: ' .. f_res)
+ dbg_print('search', 'Levenshtein distance: ' .. f_lev)
+ end
+ end
+
+ -- finally, sort results
+ texdoc.score.sort_doclist(s_doclist, pattern)
+ return s_doclist
+end
+
+return M
+
+-- vim: ft=lua:
diff --git a/support/texdoc/script/texdoclib-util.tlu b/support/texdoc/script/texdoclib-util.tlu
new file mode 100644
index 0000000000..d14a212a12
--- /dev/null
+++ b/support/texdoc/script/texdoclib-util.tlu
@@ -0,0 +1,127 @@
+-- texdoclib-util.tlu: utility functions for texdoc
+--
+-- The TeX Live Team, GPLv3, see texdoclib.tlu for details
+
+-- dependencies
+local texdoc = {
+ const = require('texdoclib-const'),
+ config = require('texdoclib-config'),
+}
+
+-- shortcuts
+local M = {}
+local C = texdoc.const
+
+-- change '/' to '\' on windows
+-- Note: Internal representation of files always use forward slashes.
+-- This function should be called only before displaying a path.
+if os.type == 'windows' then
+ function M.w32_path(path)
+ return (string.gsub(path, '/', '\\'))
+ end
+else
+ function M.w32_path(path)
+ return path
+ end
+end
+
+-- remove the last directory component of a path
+if os.type == 'windows' then
+ function M.path_parent(path)
+ return string.match(path, '^(.*)[\\/]')
+ end
+else
+ function M.path_parent(path)
+ return string.match(path, '^(.*)/')
+ end
+end
+
+-- generic log display function
+local function log(label, msg, ...)
+ io.stderr:write('texdoc ' .. label .. ': ' .. msg:format(...) .. '\n')
+end
+
+-- generic error display function (see the err_priority constant)
+function M.err_print(lvl, msg, ...)
+ -- be careful: maybe config item "verbosity_level" has not set yet
+ local verbosity_level = texdoc.config.get_value('verbosity_level')
+ or tonumber(C.def_verbosity)
+
+ -- print if the priority is higher than current verbosity level
+ if C.err_priority[lvl] <= verbosity_level then
+ log(lvl, msg, ...)
+ end
+end
+local err_print = M.err_print
+
+do -- begin scope of active_debugs
+local active_debugs
+
+-- set active_debugs
+local function set_active_debugs()
+ local debug_list = texdoc.config.get_value('debug_list')
+
+ if not debug_list then return end
+ active_debugs = {}
+
+ -- all debug options imply version info
+ if debug_list[1] then
+ active_debugs.version = true
+ else
+ return
+ end
+
+ -- if 'all' is the first keyword, just activate all categories
+ if debug_list[1] == 'all' then
+ for dbg in pairs(C.known_debugs) do
+ active_debugs[dbg] = true end
+ return
+ end
+
+ -- activate options from the list
+ for _, dbg in ipairs(debug_list) do
+ local deps = C.known_debugs[dbg]
+ if deps then
+ active_debugs[dbg] = true
+ for _, d in ipairs(deps) do active_debugs[d] = true end
+ else
+ err_print('warning', 'Unknown debug category "' .. dbg .. '".')
+ end
+ end
+end
+
+-- generic debug function
+function M.dbg_print(cat, msg, ...)
+ -- make sure active_debugs is set
+ if not active_debugs then set_active_debugs() end
+
+ -- print message it belongs to an active category
+ if active_debugs and active_debugs[cat] or cat == 'XXX' then
+ log('debug-' .. cat, msg, ...)
+ end
+end
+
+end -- end scope of active_debugs
+
+-- if file is base .. '.' .. zip with zip in zipext_list, return: base, zip
+-- otherwise, return: file, nil
+function M.parse_zip(file)
+ local zip
+ for _, zip in ipairs(texdoc.config.get_value('zipext_list')) do
+ local l = #zip + 1
+ if string.sub(file, -l, -1) == '.' .. zip then
+ return string.sub(file, 1, -l - 1), zip
+ end
+ end
+
+ return file, nil
+end
+
+-- print a usage message
+function M.print_usage()
+ print(C.usage_msg)
+end
+
+return M
+
+-- vim: ft=lua:
diff --git a/support/texdoc/script/texdoclib-view.tlu b/support/texdoc/script/texdoclib-view.tlu
new file mode 100644
index 0000000000..126ca57686
--- /dev/null
+++ b/support/texdoc/script/texdoclib-view.tlu
@@ -0,0 +1,246 @@
+-- texdoclib-view.tlu: view a document and/or display the list of results in texdoc
+--
+-- The TeX Live Team, GPLv3, see texdoclib.tlu for details
+
+-- dependencies
+local texdoc = {
+ const = require('texdoclib-const'),
+ util = require('texdoclib-util'),
+ config = require('texdoclib-config'),
+}
+
+-- shortcuts
+local M = {}
+local C = texdoc.const
+local err_print = texdoc.util.err_print
+local dbg_print = texdoc.util.dbg_print
+
+----------------------------- view a document -----------------------------
+
+-- view a document
+-- see texdoclib-search.tlu for the structure of the argument
+local function view_doc(docfile)
+ M.view_file(docfile.realpath)
+end
+
+-- view a file, if possible
+local function try_viewing(view_command, viewer_replacement)
+ if string.match (view_command, C.place_holder) then
+ view_command = string.gsub(
+ view_command, C.place_holder, viewer_replacement)
+ else
+ view_command = view_command .. ' ' .. viewer_replacement
+ end
+
+ -- try to catch problems with missing DISPLAY on Unix
+ if os.type == 'unix' and not (os.name == 'macosx')
+ and os.getenv('DISPLAY') == nil then
+ err_print('warning',
+ 'DISPLAY is not set; your viewer will likely have problems.')
+ err_print('warning',
+ 'Try --list to list results instead of displaying them.')
+ end
+ err_print('info', 'View command: ' .. view_command)
+
+ -- See long comment below this function for the LC_CTYPE story.
+ -- We only want to reset the environment if we have the value
+ -- to reset it to. In older versions of luatex, status.lc_* will be nil.
+ local env_lc_ctype = status.lc_ctype
+ local luatex_lc_ctype = os.setlocale(nil, 'ctype')
+ if (env_lc_ctype) then
+ err_print('info', 'Setting environment LC_CTYPE to: ' .. env_lc_ctype)
+ os.setenv('LC_CTYPE', env_lc_ctype)
+ end
+
+ -- the big casino: run the external command.
+ if os.execute(view_command) > 0 then
+ err_print('error', 'Failed to execute: ' .. view_command)
+ os.exit(C.exit_error)
+ end
+
+ -- reset back to "C" (should always be C and always happen, but in case...)
+ if (luatex_lc_ctype) then
+ os.setenv('LC_CTYPE', luatex_lc_ctype)
+ end
+end
+
+-- get viewer and viewer_replacement before calling try_viewing
+-- returns false of failure, true on success viewer_replacement is either:
+--
+-- 1. the filename, quoted with ""
+-- 2. the filename, quoted with "" and followed by some rm commands
+--
+-- The second case happens when the doc is zipped. In the case, this function
+-- unzips it in a tmpdir so that the viewer command can use the unzipped file.
+function M.view_file(filename)
+ local viewer, viewer_replacement
+
+ -- check if the file is zipped
+ local nozipname, zipext = texdoc.util.parse_zip(filename)
+
+ -- determine viewer_replacement
+ if zipext then
+ local unzip_cmd = texdoc.config.get_value('unzip_' .. zipext)
+
+ if not unzip_cmd then
+ err_print('error', 'No unzip command for ".%s" files.', zipext)
+ os.exit(C.exit_error)
+ end
+
+ local tmpdir = os.tmpdir('/tmp/texdoc.XXXXXX')
+ if not tmpdir then
+ err_print('error', 'Failed to create tempdir to unzip.')
+ os.exit(C.exit_error)
+ end
+
+ local basename = string.match(nozipname, '.*/(.*)$') or nozipname
+ local tmpfile = '"' .. tmpdir .. '/' .. basename .. '"'
+ local unzip = unzip_cmd .. ' "' .. filename .. '">' .. tmpfile
+ dbg_print('view', 'Unzip command: ' .. unzip)
+
+ if not os.execute(unzip) then
+ err_print('error', 'Failed to unzip ' .. filename)
+ os.remove(tmpfile)
+ os.remove(tmpdir)
+ os.exit(C.exit_error)
+ end
+
+ -- it is necessary to sleep a few secounds. Otherwise, the temporary
+ -- file could be removed before opening it.
+ viewer_replacement = tmpfile .. '; sleep 2; ' ..
+ texdoc.config.get_value('rm_file') .. ' ' ..tmpfile .. '; ' ..
+ texdoc.config.get_value('rm_dir') .. ' ' .. tmpdir
+ filename = nozipname
+ else
+ viewer_replacement = '"' .. texdoc.util.w32_path(filename) .. '"'
+ end
+
+ -- files without extension are assumed to be text
+ local viewext = (filename:match('.*%.([^/]*)$') or 'txt'):lower()
+
+ -- special case : sty files use txt viewer
+ -- FIXME: hardcoding such cases this way is not very clean
+ if viewext == 'sty' then viewext = 'txt' end
+ if viewext == 'texlive' then viewext = 'txt' end
+ if viewext == 'htm' then viewext = 'html' end
+
+ -- get a viewer from built-in defaults if not already set
+ if not texdoc.config.get_value('viewer_' .. viewext) then
+ texdoc.config.get_default_viewers()
+ end
+
+ -- still no viewers? use txt as a fallback
+ if not texdoc.config.get_value('viewer_' .. viewext) then
+ err_print('warning',
+ 'No viewer defined for ".%s" files, using "viewer_txt" instead.',
+ viewext)
+ viewext = 'txt'
+ end
+
+ -- finally, check validity of the viewer
+ viewer = texdoc.config.get_value('viewer_' .. viewext)
+ assert(viewer, 'Internal error: no viewer found.')
+ dbg_print('view', 'Using "viewer_%s" to open the file.', viewext )
+
+ return try_viewing(viewer, viewer_replacement)
+end
+
+-- Explanation of locale madness:
+-- LuaTeX resets LC_CTYPE, LC_COLLATE, LC_NUMERIC to "C". That is good for
+-- inside luatex, but when we run an external program, if the user's
+-- environment is something else, we want to switch back to it. As of
+-- TL 2017 LuaTeX, we can inspect the user's locale with status.lc_ctype, etc.
+--
+-- For texdoc purposes, what matters is LC_CTYPE (so we don't bother with
+-- the others). For example, with the less pager, when LC_CTYPE=C,
+-- non-ASCII bytes are displayed as "<xx>", where xx is the two hex
+-- digits for the byte.
+
+----------------------------- display results -----------------------------
+
+-- print a list of docfile objects (see texdoclib-search.tlu) as a menu
+-- if showall is false, stop as soon as a bad result is encountered
+local function print_menu(name, doclist, showall)
+ local max_lines = tonumber(texdoc.config.get_value('max_lines'))
+ if texdoc.config.get_value('interact_switch') and doclist[max_lines + 1] then
+ -- there may be too many lines, count them
+ local n = 0
+ for _, doc in pairs(doclist) do
+ if doc.quality == 'good' or
+ (showall and doc.quality ~= 'killed') then
+ n = n + 1
+ end
+ end
+
+ if n > max_lines then
+ io.write(n, ' results. Display them all? (y/N) ')
+ local ans = io.read('*line')
+ if not ((ans == 'y') or (ans == 'Y')
+ -- io.read had a bug wrt windows eol on some versions of texlua
+ or (ans == '\ry') or (ans == '\rY')) then
+ return
+ end
+ end
+ end
+ local i, doc, last_i
+ for i, doc in ipairs(doclist) do
+ if doc.quality == 'killed' then break end
+ if doc.quality ~= 'good' and not showall then break end
+ if texdoc.config.get_value('machine_switch') == true then
+ print(name, doc.score, texdoc.util.w32_path(doc.realpath),
+ doc.lang or '', doc.details or '')
+ else
+ last_i = i -- save for test below
+ print(string.format('%2d %s', i, texdoc.util.w32_path(doc.realpath)))
+ if doc.details or doc.lang then
+ local line = ' = '
+ if doc.lang then line = line .. '[' .. doc.lang .. '] ' end
+ if doc.details then line = line .. doc.details end
+ print(line)
+ end
+ end
+ end
+ if texdoc.config.get_value('interact_switch') then
+ io.write('Enter number of file to view, RET to view 1, anything else to skip: ')
+ local num_str = io.read('*line')
+ -- That returns the empty string on an empty line, nil on EOF.
+ -- We only want to default to viewing 1 on an empty line.
+ -- Use Lua's faked ternary operator for fun and brevity:
+ num = (num_str == '' and 1 or tonumber(num_str))
+ if num and doclist[num] and num <= last_i then
+ view_doc(doclist[num])
+ end
+ end
+end
+
+----------------------- deliver results based on mode ---------------------
+
+function M.deliver_results(name, doclist, many)
+ -- ensure that results were found or apologize
+ if not doclist[1] or doclist[1].quality == 'killed' then
+ local msg = string.gsub(C.notfound_msg, C.notfound_msg_ph, name)
+ io.stderr:write(msg .. '\n') -- get rid of gsub's 2nd value
+ os.exit(C.exit_notfound)
+ end
+ -- shall we show all of them or only the "good" ones?
+ local showall = (texdoc.config.get_value('mode') == 'showall')
+ if not showall and doclist[1].quality ~= 'good' then
+ showall = true
+ err_print('info', 'No good result found, showing all results.')
+ end
+ -- view result or show menu based on mode and number of results
+ if (texdoc.config.get_value('mode') == 'view')
+ or texdoc.config.get_value('mode') == 'mixed' and (not doclist[2]
+ or (doclist[2].quality ~= 'good' and not showall)) then
+ view_doc(doclist[1])
+ else
+ if many and not texdoc.config.get_value('machine_switch') then
+ print('*** Results for: ' .. name .. ' ***')
+ end
+ print_menu(name, doclist, showall)
+ end
+end
+
+return M
+
+-- vim: ft=lua:
diff --git a/support/texdoc/script/texdoclib.tlu b/support/texdoc/script/texdoclib.tlu
new file mode 100644
index 0000000000..fb85895d37
--- /dev/null
+++ b/support/texdoc/script/texdoclib.tlu
@@ -0,0 +1,37 @@
+-- texdoclib.tlu: the texdoc library
+
+--[[
+Copyright 2008 Manuel Pégourié-Gonnard, Takuto Asakura, and the TeX Live Team.
+
+This program is free software: you can redistribute it and/or modify it under
+the terms of the GNU General Public License as published by the Free Software
+Foundation, either version 3 of the License, or (at your option) any later
+version.
+
+This program is distributed in the hope that it will be useful, but WITHOUT ANY
+WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License along with
+this program. If not, see <https://www.gnu.org/licenses/>.
+
+Previous work in the public domain:
+- Contributions from Reinhard Kotucha (2008).
+- First texlua versions by Frank Küster (2007).
+- Original shell script by Thomas Esser, David Aspinall, and Simon Wilkinson.
+--]]
+
+local texdoc = {
+ const = require('texdoclib-const'),
+ util = require('texdoclib-util'),
+ alias = require('texdoclib-alias'),
+ score = require('texdoclib-score'),
+ config = require('texdoclib-config'),
+ search = require('texdoclib-search'),
+ view = require('texdoclib-view'),
+ cli = require('texdoclib-cli'),
+}
+
+return texdoc
+
+-- vim: ft=lua: