From e0c6872cf40896c7be36b11dcc744620f10adf1d Mon Sep 17 00:00:00 2001 From: Norbert Preining Date: Mon, 2 Sep 2019 13:46:59 +0900 Subject: Initial commit --- support/texdoc/script/texdoc.tlu | 17 + support/texdoc/script/texdoclib-alias.tlu | 127 +++++ support/texdoc/script/texdoclib-cli.tlu | 219 ++++++++ support/texdoc/script/texdoclib-config.tlu | 559 +++++++++++++++++++++ support/texdoc/script/texdoclib-const.tlu | 158 ++++++ support/texdoc/script/texdoclib-score.tlu | 293 +++++++++++ support/texdoc/script/texdoclib-search.tlu | 771 +++++++++++++++++++++++++++++ support/texdoc/script/texdoclib-util.tlu | 127 +++++ support/texdoc/script/texdoclib-view.tlu | 246 +++++++++ support/texdoc/script/texdoclib.tlu | 37 ++ 10 files changed, 2554 insertions(+) create mode 100755 support/texdoc/script/texdoc.tlu create mode 100644 support/texdoc/script/texdoclib-alias.tlu create mode 100644 support/texdoc/script/texdoclib-cli.tlu create mode 100644 support/texdoc/script/texdoclib-config.tlu create mode 100644 support/texdoc/script/texdoclib-const.tlu create mode 100644 support/texdoc/script/texdoclib-score.tlu create mode 100644 support/texdoc/script/texdoclib-search.tlu create mode 100644 support/texdoc/script/texdoclib-util.tlu create mode 100644 support/texdoc/script/texdoclib-view.tlu create mode 100644 support/texdoc/script/texdoclib.tlu (limited to 'support/texdoc/script') 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 = { stop, aliasentry1, ...}, + ... +} +stop == true means further alias directives should be ignored + +aliasentry = { + name = pattern to be matched, + score = associated score, + original = is this the original keyword?, + locale = 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: +Repository: +Please email bugs to .]] + +copyright_msg = [[ +Copyright 2008 Manuel Pégourié-Gonnard, Takuto Asakura, and the TeX Live Team. +License GPLv3+: GNU GPL version 3 or later . +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: ]] +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 . +-- 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_() 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 "", 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 . + +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: -- cgit v1.2.3