diff options
author | Manuel Pégourié-Gonnard <mpg@elzevir.fr> | 2009-11-24 19:46:08 +0000 |
---|---|---|
committer | Manuel Pégourié-Gonnard <mpg@elzevir.fr> | 2009-11-24 19:46:08 +0000 |
commit | aa1420a6770a5148a153b2b1e4283fb91a9a3d3b (patch) | |
tree | d9582016d27249d388221be878ae5b3c2e6d6265 /Master/texmf/scripts/texdoc | |
parent | 01da98ba6f4010aa1cf6163e019c8ebeb24f68bd (diff) |
Texdoc 0.60.
git-svn-id: svn://tug.org/texlive/trunk@16153 c570f23f-e606-0410-a88d-b1316a301751
Diffstat (limited to 'Master/texmf/scripts/texdoc')
-rw-r--r-- | Master/texmf/scripts/texdoc/config.tlu | 126 | ||||
-rw-r--r-- | Master/texmf/scripts/texdoc/constants.tlu | 36 | ||||
-rw-r--r-- | Master/texmf/scripts/texdoc/functions.tlu | 48 | ||||
-rw-r--r-- | Master/texmf/scripts/texdoc/main.tlu | 15 | ||||
-rw-r--r-- | Master/texmf/scripts/texdoc/score.tlu | 226 | ||||
-rw-r--r-- | Master/texmf/scripts/texdoc/search.tlu | 364 | ||||
-rwxr-xr-x | Master/texmf/scripts/texdoc/texdoc.tlu | 11 | ||||
-rw-r--r-- | Master/texmf/scripts/texdoc/view.tlu | 77 |
8 files changed, 643 insertions, 260 deletions
diff --git a/Master/texmf/scripts/texdoc/config.tlu b/Master/texmf/scripts/texdoc/config.tlu index 1098d39b7ea..304ae9d3dca 100644 --- a/Master/texmf/scripts/texdoc/config.tlu +++ b/Master/texmf/scripts/texdoc/config.tlu @@ -9,16 +9,17 @@ local L = {} load_env(L, { 'export_symbols', 'string', 'table', 'os', 'kpse', 'lfs', 'io', - 'arg', 'ipairs', 'pairs', 'tonumber', 'tostring', 'setmetatable', 'next', 'print', 'assert', 'error', + 'err_print', 'win32_hook', 'deb_print', + 'confline_to_alias', 'confline_to_score', 'C', - 'err_print', 'win32_hook', - 'config', 'alias' + 'config', }) ------------------------ hide config and alias tables ----------------------- +-------------------------- hide the config table --------------------------- +-- config is read-only function set_read_only(table, name) assert(next(table) == nil, 'Internal error: '..name..' should be empty at this point.') @@ -32,9 +33,9 @@ function set_read_only(table, name) end real_set_config = set_read_only(config, 'config') -real_set_alias = set_read_only(alias, 'alias') ----------------------------- general functions ----------------------------- + +------------------------- general config functions ------------------------- -- set a config parameter, but don't overwrite it if already set -- three special types: *_list (list), *_switch (boolean), *_level (number) @@ -82,13 +83,13 @@ function set_config_element (key, value, context) else -- string real_set_config(key, value) end - -- special case: if we just set verbosity_level, print version info now - if key == 'verbosity_level' then - err_print(arg[0]..' version '..C.version, 'debug1') + -- special case: if we just set debug_list, print version info now + if key == 'debug_list' then + deb_print('version', C.fullname..' version '..C.version) end -- now tell what we have just done, for debugging - err_print('Setting "'..key..'='..value..'" ' - ..context_to_string(context)..'.', 'debug2') + deb_print('config', + "Setting '"..key.."="..value.."' "..context_to_string(context)..'.') end -- a helper function for warning messages in the above @@ -97,7 +98,7 @@ function config_warn (key, value, context, unknown) and 'Unknown option "'..key..'"' or 'Illegal value "'..tostring(value)..'" for option "'..key..'"' local ending = '. Skipping.' - err_print (begin..' '..context_to_string(context)..ending, 'warning') + err_print('warning', begin..' '..context_to_string(context)..ending) end -- interpreting 'context' for the previous functions @@ -123,19 +124,12 @@ function set_config_list (conf, context) end end --- set an alias (w/o overwriting) -function set_alias (key, value) - if alias[key] == nil then - real_set_alias(key, value) - end -end - ------------------------ options from command line ------------------------- -- set config from the command line -- Please make sure to update C.usage_msg accordingly -- and set a default value in setup_config_from_defaults() if relevant. -function setup_config_from_cl () +function setup_config_from_cl(arg) local curr_arg local function set_config_elt(key, val) set_config_element(key, val, {src='cl', name=curr_arg}) @@ -161,8 +155,12 @@ function setup_config_from_cl () set_config_elt('mode', 'mixed') elseif (curr_arg == '-l') or (curr_arg == '--list') then set_config_elt('mode', 'list') - elseif (curr_arg == '-s') or (curr_arg == '--search') then - set_config_elt ('mode', 'search') + elseif (curr_arg == '-s') or (curr_arg == '--showall') then + set_config_elt ('mode', 'showall') + elseif (curr_arg == '--search') then + set_config_elt ('mode', 'showall') + err_print('warning', '--search has been renamed to --showall') + err_print('warning', 'Please use the new name.') elseif (curr_arg == '-r') or (curr_arg == '--regex') then set_config_elt ('mode', 'regex') -- interaction @@ -178,25 +176,44 @@ function setup_config_from_cl () set_config_elt('alias_switch', 'false') elseif (curr_arg == '-a') or (curr_arg == '--alias') then set_config_elt('alias_switch', 'true') - -- verbosity + -- debug elseif (curr_arg == '-d') or (curr_arg == '--debug') then - set_config_elt('verbosity_level', C.err_max) + set_config_elt('debug_list', 'all') + elseif string.match(curr_arg, '^%-d=') then + local value = string.gsub(curr_arg, '^%-d=', '') + set_config_elt('debug_list', value) + elseif string.match(curr_arg, '^%-%-debug=') then + local value = string.gsub(curr_arg, '^%-%-debug=', '') + set_config_elt('debug_list', value) + -- verbosity elseif string.match(curr_arg, '^%-v') then local value = string.gsub(curr_arg, '^%-v=?', '') set_config_elt('verbosity_level', value) elseif string.match(curr_arg, '^%-%-verbosity') then local value = string.gsub(curr_arg, '^%-%-verbosity=?', '') set_config_elt('verbosity_level', value) - -- extensions list + -- extensions list (deprecated) elseif string.match(curr_arg, '^%-e') then local value = string.gsub(curr_arg, '^%-e=?', '') set_config_elt('ext_list', value) + err_print('warning', + '-e is deprecated and will likely be removed soon.') + err_print('warning', + 'ext_list should be set in a configuration file instead.') + err_print('warning', + 'Please protest on the texdoc mailing list if you need this.') elseif string.match(curr_arg, '^%-%-extensions') then local value = string.gsub(curr_arg, '^%-%-extensions=?', '') set_config_elt('ext_list', value) + err_print('warning', + '--extensions is deprecated and will likely be removed soon.') + err_print('warning', + 'ext_list should be set in a configuration file instead.') + err_print('warning', + 'Please protest on the texdoc mailing list if you need this.') -- problem else - err_print ("unknown option: "..curr_arg, "error") + err_print('error', "unknown option: "..curr_arg) print (C.error_msg) os.exit(2) end @@ -234,32 +251,37 @@ function read_config_file(configfile) local cnf = assert(io.open(configfile, 'r')) local lineno = 0 while true do - local key, val 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 - key, val = string.match(line, '^([%a%d_]+)%s*=%s*(.+)') - if key and val then - set_config_element(key, val, { - src='file', file=configfile, line=lineno}) - else - key, val = string.match(line, '^alias%s+([%a%d_-]+)%s*=%s*(.+)') - if key and val then - set_alias(key, val) - else - if (not string.match (line, '^%s*$')) then - err_print ('syntax error in '..configfile.. - ' at line '..lineno..'.', 'warning') - end - end + -- 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 '..configfile..' at line '..lineno..'.') end end cnf:close() end +-- interpret a confline as a config setting or return false +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 + + -- return a table with config file and if they exist function get_config_files () local platform = string.match (kpse.var_value ('SELFAUTOLOC'), '.*/(.*)$') @@ -442,7 +464,8 @@ function setup_config_from_defaults() interact_switch = 'true', machine_switch = 'false', verbosity_level = '2', - ext_list = 'pdf, html, txt, man1.pdf, man5.pdf, ps, dvi, ', + ext_list = 'pdf, html, txt, ps, dvi, ', + badext_list = 'txt, ', } -- must be set after mode! set_config_elt ('alias_switch', alias_from_mode(config.mode)) @@ -470,14 +493,25 @@ end -------------------------- set all configuration --------------------------- -- populate the config and alias arrays -function setup_config_and_alias() +function setup_config_and_alias(arg) -- setup config from all sources - setup_config_from_cl() + setup_config_from_cl(arg) setup_config_from_env() setup_config_from_files() setup_config_from_defaults() - -- we were waiting for config.verbosity_level to be know to do this - show_config_files(function(s) err_print(s, 'debug1') end) + -- regex mode is deprecated + if config.mode == 'regex' then + err_print('warning', + 'Regex mode is deprecated and will likely be removed soon.') + err_print('warning', + 'Please protest on the texdoc mailing list if you need this.') + end + -- machine mode implies no interaction + if config.machine_switch == true then + real_set_config('interact_switch', false) + end + -- we were waiting for config.debug_list to be know to do this + show_config_files(function(s) deb_print('files', s) end) end -- finally export a few symbols diff --git a/Master/texmf/scripts/texdoc/constants.tlu b/Master/texmf/scripts/texdoc/constants.tlu index 6ff1fb079e1..b1a0bcc4e19 100644 --- a/Master/texmf/scripts/texdoc/constants.tlu +++ b/Master/texmf/scripts/texdoc/constants.tlu @@ -8,12 +8,14 @@ See texdoc.tlu for details. local L = {} load_env(L, { 'setmetatable', 'next', 'assert', 'error', + 'arg', 'C', }) -- progname and version -progname = 'texdoc' -version = '0.50' +fullname = arg[0] +progname = 'texdoc' +version = '0.60' -- make sure to update setup_config_from_cl() accordingly -- and set a default value in setup_config_from_defaults() if relevant @@ -23,18 +25,18 @@ With no NAME, it can print configuration information (-f, --files); the usual --help and --version options are also accepted. Usage: texdoc [OPTIONS]... [NAME]... -f, --files Print the name of the config files being used. - -e, --extensions=L Require file extensions to be in the list L. -w, --view Use view mode: start a viewer. -m, --mixed Use mixed mode (view or list). - -l, --list Use list mode: don't start a viewer. - -s, --search Search for name as a substring. - -r, --regex Search for name as a lua regex. + -l, --list Use list mode: show a list of results. + -s, --showall Use showall mode: show also "bad" results. + -r, --regex Use regex mode. (Deprecated.) + -e, --extensions=L Set ext_list=L. (Deprecated.) -a, --alias Use the alias table. -A, --noalias Don't use the alias table. -i, --interact Use interactive menus. -I, --nointeract Use plain lists, no interaction required. -v, --verbosity=N Set verbosity level to N. - -d, --debug Set verbosity level to maximum. + -d, --debug[=list] Activate debug for selected items (default all). -M, --machine Use a more machine-friendly output format. Environment: PAGER, BROWSER, PDFVIEWER, PSVIEWER, DVIVIEWER. Files: <texmf>/texdoc/texdoc.cnf files, see the -f option. @@ -56,7 +58,9 @@ known_options = { 'machine_switch', 'alias_switch', 'ext_list', + 'badext_list', 'verbosity_level', + 'debug_list', 'lastfile_switch', 'rm_dir', 'rm_file', @@ -68,13 +72,19 @@ err_priority = { error = 1, warning = 2, info = 3, - debug1 = 4, - debug2 = 5, - debug3 = 6, - debug4 = 7, - debug5 = 8, } -err_max = 8 + +known_debugs = { + version = {}, + files = {}, + config = {'files'}, + view = {}, + texdocs = {}, + filesea = {}, + lsrsea = {}, + kpse = {'texdocs', 'filesea', 'lsrsea'}, + score = {}, +} place_holder = '%%s' -- used for viewer commands diff --git a/Master/texmf/scripts/texdoc/functions.tlu b/Master/texmf/scripts/texdoc/functions.tlu index faee493f156..4db39c2482f 100644 --- a/Master/texmf/scripts/texdoc/functions.tlu +++ b/Master/texmf/scripts/texdoc/functions.tlu @@ -9,7 +9,7 @@ local L = {} load_env(L, { 'export_symbols', 'string', 'io', 'os', - 'ipairs', + 'pairs', 'ipairs', 'C', 'config', }) @@ -27,7 +27,7 @@ else end -- generic error display function (see the error_priority constant) -function err_print (msg, lvl) +function err_print (lvl, msg) -- be careful: maybe config.verbosity_level is not set yet local verbosity_level = config.verbosity_level or 2 if C.err_priority[lvl] <= verbosity_level then @@ -35,6 +35,49 @@ function err_print (msg, lvl) end end +do --scope of active_debugs +local active_debugs + +-- generic debug function +function deb_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 + io.stderr:write ("texdoc debug-"..cat..": "..msg.."\n") + end +end + +-- set active_debugs +function set_active_debugs() + if not config.debug_list then return end + active_debugs = {} + -- all debug options imply version info + if config.debug_list[1] then + active_debugs.version = true + else + return + end + -- if 'all' is the first keyword, just activate all categories + if config.debug_list[1] == 'all' then + for deb in pairs(C.known_debugs) do + active_debugs[deb] = true end + return + end + -- activate options from the list + for _, deb in ipairs(config.debug_list) do + local deps = C.known_debugs[deb] + if deps then + active_debugs[deb] = true + for _, d in ipairs(deps) do active_debugs[d] = true end + else + err_print('warning', "Unknown debug category '"..deb.."'.") + end + end +end + +end -- scope of active_debugs + -- if zip is support and file is base..'.'..zip with zip in zipext_list, -- return base, zip -- otherwise, returns file, nil function parse_zip(file) @@ -53,6 +96,7 @@ end -- finally export a few symbols export_symbols(L, { 'err_print', + 'deb_print', 'win32_hook', 'parse_zip', }) diff --git a/Master/texmf/scripts/texdoc/main.tlu b/Master/texmf/scripts/texdoc/main.tlu index 1d538904662..c5ff50d2ded 100644 --- a/Master/texmf/scripts/texdoc/main.tlu +++ b/Master/texmf/scripts/texdoc/main.tlu @@ -11,19 +11,17 @@ load_env(L, { 'print', 'ipairs', 'arg', 'C', - 'config', 'setup_config_and_alias', - 'get_docfiles', - 'sort_docfiles', + 'get_doclist', 'deliver_results', }) -- setup config options and aliases from various places -setup_config_and_alias() +setup_config_and_alias(arg) -- make sure we actually have argument(s) if not arg[1] then - print (C.usage_msg) + print(C.usage_msg) os.exit(2) end @@ -32,9 +30,8 @@ local docname for _, docname in ipairs(arg) do -- do we have more then one argument? local multiarg = not not arg[2] - -- get results and sort them - local docfiles = get_docfiles(docname) - sort_docfiles(docfiles) + -- get results + local doclist = get_doclist(docname) -- deliver results to the user - deliver_results(docname, docfiles, multiarg) + deliver_results(docname, doclist, multiarg) end diff --git a/Master/texmf/scripts/texdoc/score.tlu b/Master/texmf/scripts/texdoc/score.tlu index 734f4b5b7d8..6c8bc2c77f2 100644 --- a/Master/texmf/scripts/texdoc/score.tlu +++ b/Master/texmf/scripts/texdoc/score.tlu @@ -9,29 +9,193 @@ local L = {} load_env(L, { 'export_symbols', 'string', 'table', - 'ipairs', - 'config', 'parse_zip', + 'pairs', 'ipairs', 'tostring', 'tonumber', 'assert', + 'config', 'parse_zip', 'err_print', 'deb_print', }) --- sort docfiles -function sort_docfiles(df) - table.sort(df, docfile_order) +-- shared variables +local global_adjscore, spec_adjscore = {}, {} + +------------------------- configuration directives ------------------------- + +-- interpret a confline as a score directive or return false +function 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 --- compare docfiles: (see search.tlu for structure) --- 1. exact is better than non-exact, --- 2. then extensions are ordered as in ext_list, --- 3. then trees, --- 4. then filenames lexicographically. --- return true is a is better than b -function docfile_order (a, b) - if a.exact and not b.exact then +-- set key in score table to val, without overriding +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 - elseif b.exact and not a.exact then - return false - elseif a.tree < b.tree then + end + return false +end + +---------------------------- score computation ----------------------------- + +-- set the scores for a doclist +function set_list_scores(list, original_kw) + for _, df in ipairs(list) do + set_score(df, original_kw) + end +end + +-- set the score of a docfile +function set_score(df, original_kw) + -- scoring is irrelevant in regex mode + if config.mode == 'regex' then df.score = 0 return end + deb_print('score', 'Start scoring '..df.realpath) + deb_print('score', 'Name used: '..df.name) + -- scoring is case-insenstitive (patterns are already lowercased) + local name = string.lower(df.name) + -- get score from patterns + local score = 0 + for _, pat in ipairs(df.matches) do + local s = 0 + local p = string.lower(pat.name) + if pat.original then + s = df.tree > -1 and heuristic_score(name, p) or 1 + elseif is_exact(name, p) then + s = pat.score or 10 -- default alias score + deb_print('score', string.format( + "Matching alias '%s', score: %d", pat.name, s)) + end + if s > score then score = s end + end + deb_print('score', 'Max pattern score: '..tostring(score)) + -- 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 + if score > 0 or val < 0 then score = score + val end + deb_print('score', string.format( + "Adjust by '%d' 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 > 0 or val < 0 then score = score + val end + deb_print('score', string.format( + "Adjust by '%d' from global pattern '%s'", val, pat)) + end + end + end + deb_print('score', 'Final score: '..tostring(score)) + df.score = score +end + +-- suffixes for heuristic match +local suffixes = { + 'doc', '-doc', '_doc', '.doc', '/doc', + 'manual', '/manual', '-manual', + '-user', + '-man', +} + +-- compute a heuristic score 0 <= s < 10 +function heuristic_score(file, pat) + deb_print('score', 'Start heuristic scoring with pattern: '..pat) + -- if extension is bad, score is 0 + local ext = config.ext_list[ext_pos(file)] + if ext and config.badext_list_inv[ext] then + deb_print('score', 'Bad extension, heuristic score 0') + return 0 + end + -- score management + local score = 0 + local function upscore(s, reason) + if s > score then + score = s + deb_print('score', 'New heuristic score: '..tostring(s) + ..'. Reason: '..reason) + end + end + local slash = not not string.find(pat, '/', 1, true) + -- look for exact or subword match + if 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(suffixes) do + local deriv = pat..suffix + if 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 + -- bonus for being in the right directory + if score > 0 and not slash + and string.find('/'..file, '/'..pat..'/', 1, true) then + upscore(score + 0.5, 'directory bonus') + end + -- done + deb_print('score', 'Final heuristic score: '..tostring(score)) + return score +end + +-- says if file is an exact match for pat +function is_exact(file, pat) + file = 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(config.ext_list) do + if ext ~= '' and ext ~= '*' and basename == pat..'.'..ext then + return true + end + end + return false +end + +-- say if pat is a "subword" of str +function is_subword(str, pat) + 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 + +-- say if character i of str is a delimiter (ponctuation) +function is_delim(str, i) + return not not string.find(string.sub(str, i, i), '%p') +end + +-- compare two docfile's: (see search.tlu for structure) +-- 1. by score +-- 2. then by extensions (ordered as in ext_list), +-- 3. then by tree, +-- 4. then lexicographically by filename. +-- return true if a is better than b +function docfile_order (a, b) + if a.score > b.score then return true - elseif b.tree < a.tree then + elseif a.score < b.score then return false else a.ext_pos = a.ext_pos or ext_pos(a.name) @@ -40,6 +204,10 @@ function docfile_order (a, b) return true elseif a.ext_pos > b.ext_pos then return false + elseif a.tree > b.tree then + return true + elseif a.tree < b.tree then + return false else return (a.name < b.name) end @@ -68,7 +236,29 @@ function ext_pos(file) return pos or (config.ext_list_max + 1) end +----------------------------- public functions ----------------------------- + +-- return the "quality" of docfile +function 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 sort_doclist(dl, original_kw) + dl:stop() + set_list_scores(dl, original_kw) + table.sort(dl, docfile_order) +end + -- export a few symbols export_symbols(L, { - 'sort_docfiles', + 'sort_doclist', + 'docfile_quality', + 'confline_to_score', }) diff --git a/Master/texmf/scripts/texdoc/search.tlu b/Master/texmf/scripts/texdoc/search.tlu index fcb044d5189..fa35cce2754 100644 --- a/Master/texmf/scripts/texdoc/search.tlu +++ b/Master/texmf/scripts/texdoc/search.tlu @@ -9,25 +9,115 @@ local L = {} load_env(L, { 'export_symbols', 'os', 'string', 'table', 'lfs', 'kpse', 'io', - 'ipairs', 'assert', 'tonumber', 'type', 'print', 'tostring', - 'err_print', 'win32_hook', 'parse_zip', - 'config', 'alias', 'C', + 'ipairs', 'assert', 'error', 'tostring', 'setmetatable', + 'deb_print', 'err_print', 'win32_hook', 'parse_zip', + 'get_patterns', 'sort_doclist', 'docfile_quality', + 'config', 'C', }) ----------------------------- the docfiles list ----------------------------- +-- shared by all functions in this file +local s_doclist --- shared by all functions below -local s_doc_files +----------------------- docfile and doclist objects ------------------------ --- structure of the s_docfiles variable --- s_docfiles = { --- [1] = docfile1, docfiles2, ..., +-- doclist = { +-- [1] = docfile1, [2] = docfiles2, ..., +-- inv = { realpath1 = index1, ... } -- } +-- +-- The inv subtable is such that for all i +-- doclist.inv(doclist[i].realpath) == i + +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) + local index = self.inv[df.realpath] + if index then + self[index]:mergein(df) + else + local newindex = #self + 1 + self[newindex] = df + self.inv[df.realpath] = newindex + end +end + +-- stops a doclist +function Doclist:stop() + self.inv = nil +end + -- docfile = { --- name = filename relative to tree, absolute if tree == nil, --- tree = number of the tree in doc_roots, --- exact = <boolean> does pattern match exactly, +-- name = filename (used for scoring only) +-- tree = code of the tree, see below +-- matches = {pattern1, pattern2, ...} -- } +-- if tree > 1, this is the index of the tree in TEXDOCS +-- if tree = 0, then name is relative to TLROOT (file found from tlpdb only) +-- tree = - 1 if and only if file is a sty file. Here name is absolute. + +local Docfile = {} +Docfile.__index = Docfile + +-- create a new docfile objet using initilisation info +-- fields : name (relative to tree), tree, pattern +function Docfile:new(info) + df = {} + setmetatable(df, self) + -- get realpath, tree and prepare name + df.tree = info.tree + if info.tree > 0 then + df.realpath = texdocs_tree_to_path(info.tree, info.name) + elseif info.tree == 0 then + error('Unimplemented') + df.realpath = get_tlroot()..'/'..info.name + info.name = string.gsub(info.name, '^texmf(-dist)?/doc/', '', 1) + elseif info.tree == -1 then + df.realpath = info.name + else + error('Internal error: bad tree number') + end + -- remove first component of name if at least two directory levels + if info.tree > -1 then + local name = string.match(info.name, '^..-/(.+/.+)$') + if name then + df.name = '/'..name + else + df.name = info.name + end + else + df.name = info.name + end + -- initialise the list of matches + if info.pattern then + df.matches = { info.pattern } + else + df.matches = {} + end + return df +end + +-- merge a second docfile objet into self +function Docfile:mergein(df) + if df.tree > self.tree then + self.name = df.name + self.tree = df.tree + end + for _, m in ipairs(df.matches) do + table.insert(self.matches, m) + end +end + +-- from score.tlu +Docfile.quality = docfile_quality ------------------ get results from TEXDOCS (à la kpse) ------------------- @@ -49,71 +139,57 @@ function get_texdocs () 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) - err_print('Search paths:', 'debug3') - for i, dir in ipairs(raw_doc_roots) do - doc_roots[i] = {} + local max = #raw_doc_roots + 1 + for j, dir in ipairs(raw_doc_roots) do + local i = max - j + local dr = {} local n + -- get path, !! and // values dir, n = string.gsub (dir, '//$', '') - doc_roots[i].recursion_allowed = (n == 1) - doc_roots[i].path, n = string.gsub (dir, '^!!', '') - doc_roots[i].index_mandatory = (n == 1) - err_print(string.format('%s (index_mandatory=%s, recursion_allowed=%s)', - doc_roots[i].path, - tostring(doc_roots[i].index_mandatory), - tostring(doc_roots[i].recursion_allowed)), - 'debug3') + dr.recursion_allowed = (n == 1) + dr.path, n = string.gsub (dir, '^!!', '') + dr.index_mandatory = (n == 1) + deb_print('texdocs', string.format( + 'texdocs[%d] = %s (index_mandatory=%s, recursion_allowed=%s)', + i, dr.path, + tostring(dr.index_mandatory), + tostring(dr.recursion_allowed))) + -- decide if we should use a ls-R index, the filesystem, or do nothing + local root, shift = lsr_root(dr.path) + if root and shift and dr.recursion_allowed then + dr.lsr = root + dr.lsr_shift = shift + deb_print('texdocs', string.format( + 'texdocs[%d] using index: %s (shift=%s)', i, root, shift)) + elseif not dr.index_mandatory and lfs.isdir(dr.path) then + dr.searchfs = true + deb_print('texdocs', string.format( + 'texdocs[%d] using filesystem search', i)) + end + -- register this in docroots + doc_roots[i] = dr end end --- return the real path of a docfile -function real_path(docfile) - if docfile.tree == nil then return docfile.name end +-- return the real path from a texdocs tree number + relative path +function texdocs_tree_to_path(tree, rel) if doc_roots == nil then get_texdocs() end - return win32_hook(doc_roots[docfile.tree].path..'/'..docfile.name) + return win32_hook(doc_roots[tree].path..'/'..rel) end -- find docfiles in texdocs directories -function get_docfiles_texdocs (pattern) - s_docfiles = {} +function get_doclist_texdocs(patlist) if doc_roots == nil then get_texdocs() end - for code, doc_root in ipairs (doc_roots) do - root, shift = lsr_root (doc_root.path) - if root and shift and doc_root.recursion_allowed then - err_print("Looking in tree '"..doc_root.path - .."' using ls-R file'" ..root.."/ls-R'.", 'debug4') - scan_lsr(root, code, shift, pattern) - elseif (not doc_root.index_mandatory) - and lfs.isdir(doc_root.path) then - err_print("Looking in tree '"..doc_root.path - .."' using filesystem.", 'debug4') - scan_tree(code, doc_root.path, '', - pattern, doc_root.recursion_allowed) + for code, dr in ipairs(doc_roots) do + if dr.lsr then + scan_lsr(patlist, code, dr.lsr, dr.lsr_shift) + elseif dr.searchfs then + scan_tree(patlist, code, dr.path, '', dr.recursion_allowed) end end - return s_docfiles -end - --- merge two components of a path, taking care of empty components -function merge_path (a, b) - return ((a == '') or (b == '')) and a..b or a..'/'..b end --- scan a tree without ls-R file -function scan_tree (code, base, cwd, pattern, recurse) - err_print("Entering directory: "..cwd, 'debug4') - for file in lfs.dir(base..'/'..cwd) do - if file ~= '.' and file ~= '..' then - local f = (cwd == '') and file or cwd..'/'..file - if lfs.isdir(base..'/'..f) then - if recurse then scan_tree(code, base, f, pattern, recurse) end - else - local df = process_file(file, f, code, pattern, true) - if df then table.insert(s_docfiles, df) end - end - end - end - err_print("Leaving directory: "..cwd, 'debug4') -end +end -- scope of doc_roots -- find a ls-R file in a parent directory an return it or nil function lsr_root (path) @@ -131,10 +207,27 @@ function lsr_root (path) end end +-- scan a tree without ls-R file +function scan_tree (patlist, code, base, cwd, recurse) + deb_print('filesea', "Entering directory: "..cwd) + for file in lfs.dir(base..'/'..cwd) do + if file ~= '.' and file ~= '..' then + local f = (cwd == '') and file or cwd..'/'..file + if lfs.isdir(base..'/'..f) then + if recurse then scan_tree(patlist, code, base, f, recurse) end + else + local df = process_file(patlist, file, f, code, true) + if df then s_doclist:add(df) end + end + end + end + deb_print('filesea', "Leaving directory: "..cwd) +end + -- scan a ls-R file -function scan_lsr (cwd, code, shift, pattern) +function scan_lsr(patlist, code, cwd, shift) local is_dir = {} -- is_dir[path] = true iff path is a dir - local results = {} + local results = Doclist:new() local isdoc = false local current_dir local l = #shift @@ -152,106 +245,125 @@ function scan_lsr (cwd, code, shift, pattern) isdoc = true current_dir = string.sub (dir_line, l+1) is_dir[current_dir] = true - err_print('Scanning directory: '..current_dir, 'debug4') + deb_print('lsrsea', 'Scanning directory: '..current_dir) elseif isdoc then - err_print("Finished scanning: "..shift, 'debug4') + deb_print('lsrsea', "Finished scanning: "..shift) break -- we're exiting the ./doc (or shift) dir, so it's over end elseif isdoc then - local df = process_file( - line, merge_path(current_dir, line), code, pattern) - if df then table.insert(results, df) end + local df = process_file(patlist, line, current_dir..'/'..line, code) + if df then results:add(df) end end end lsr:close() -- add non-directories to the list for _, df in ipairs(results) do if not is_dir[df.name] then - table.insert(s_docfiles, df) + s_doclist:add(df) end end end -end -- scope of doc_roots - ------------------------------- select results ------------------------------ - -- says if file has a 'good' extenstion according to ext_list -function check_ext(file, pattern) - local good_ext, exact_match = false, false +function check_ext(file) + file = string.lower(file) -- remove zipext if applicable file = parse_zip(file) -- then do the normal thing - local l, pat = string.len(pattern) + 1, pattern..'.' for _, e in ipairs(config.ext_list) do if e == '*' then - good_ext = true - if string.sub(file, 1, l) == pat then exact_match = true end + return true elseif (e == '') then - if not string.find(file, '.', 1, true) then good_ext = true end - if file == pattern then exact_match = true end + if not string.find(file, '.', 1, true) then + return true + end else - if string.sub(file, -string.len(e)) == e then good_ext = true end - if file == pattern..'.'..e then exact_match = true end + local dot_e = '.'..e + if string.sub(file, -string.len(dot_e)) == dot_e then + return true + end end end - return good_ext, exact_match + return false end --- return a docfile entry if it "matches", nil ortherwise -function process_file (file, pathfile, code, pattern) - err_print('Processing file: '..pathfile, 'debug5') - file = string.lower(file) - local base, ext = string.match(file, '^(.*)%.(.*)$') - if string.find(string.lower(pathfile), pattern, - 1, config.regex ~= 'regex') then - local good_ext, exact_match = check_ext(file, pattern) - err_print(string.format("File '%s' matches; good_ext=%s, exact=%s", - pathfile, tostring(good_ext), tostring(exact_match)), 'debug5') - if good_ext then - return { - name = pathfile, - tree = code, - exact = exact_match, - } +-- return a docfile object if file "matches", nil ortherwise +function process_file(patlist, file, pathfile, code) + deb_print('kpse', 'Processing file: '..pathfile) + local docfile + local pattern + for _, pattern in ipairs(patlist) do + if string.find(string.lower(pathfile), string.lower(pattern.name), + 1, config.mode ~= 'regex') then + local good_ext = check_ext(file) + deb_print('kpse', string.format( + "File '%s' matches '%s'; good_ext=%s", + pathfile, pattern.name, tostring(good_ext))) + if good_ext 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 end - return nil + return docfile end ------------------------- get results from elsewhere ------------------------ +---------------------------- look for sty files ---------------------------- --- for sty files, we obviously don't want to look in TEXDOCS... -function get_docfiles_sty (styname) - return {{ - name = kpse.find_file(styname) , - exact = true, - tree = nil, - }} +-- add doclist entries for sty files in patlist +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 ------------------------------ main function ------------------------------- -- find docfiles according to pattern -function get_docfiles(pattern) - local no_regex = (config.mode ~= 'regex') - -- apply aliases if relevant - if no_regex and config.alias_switch and alias[pattern] then - err_print (pattern.." aliased to "..alias[pattern], 'info') - pattern = alias[pattern] - end - -- search using the appropriate function - if string.match(string.lower(pattern), '%.([^/.]*)$') == 'sty' - and no_regex then - return get_docfiles_sty(pattern) - else - pattern = string.lower(pattern) - return get_docfiles_texdocs(pattern) +function get_doclist(pattern) + -- get patterns (inc. aliases) + local normal, sty = normal_vs_sty(get_patterns(pattern)) + -- initialise result list + s_doclist = Doclist:new() + -- get results + get_doclist_sty(sty) + get_doclist_texdocs(normal) + -- finally, sort results + sort_doclist(s_doclist, pattern) + return s_doclist +end + +-- separate sty patterns from the rest +function normal_vs_sty(list) + if config.mode == 'regex' then return list, {} end + 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 -- finally export a few symbols export_symbols(L, { - 'real_path', - 'get_docfiles', + 'get_doclist', }) diff --git a/Master/texmf/scripts/texdoc/texdoc.tlu b/Master/texmf/scripts/texdoc/texdoc.tlu index 013d731d1af..28470c597e7 100755 --- a/Master/texmf/scripts/texdoc/texdoc.tlu +++ b/Master/texmf/scripts/texdoc/texdoc.tlu @@ -54,17 +54,18 @@ end -- initialize kpathsea kpse.set_program_name(arg[-1], 'texdoc') --- declare global variables; they will be made read-only later +-- declare global variables; +-- they will be made read-only as soon as they are set C = {} -- constants config = {} -- configuration settings -alias = {} -- aliases -- actually load the components now -texdoc_load('constants') -- makes C read-only +texdoc_load('constants') texdoc_load('functions') -texdoc_load('config') -- makes config and alias read-only -texdoc_load('search') +texdoc_load('alias') texdoc_load('score') +texdoc_load('config') +texdoc_load('search') texdoc_load('view') -- execute main() diff --git a/Master/texmf/scripts/texdoc/view.tlu b/Master/texmf/scripts/texdoc/view.tlu index 5a94508bff2..b2f8d68a139 100644 --- a/Master/texmf/scripts/texdoc/view.tlu +++ b/Master/texmf/scripts/texdoc/view.tlu @@ -9,11 +9,10 @@ local L = {} load_env(L, { 'export_symbols', 'string', 'os', 'table', 'io', - 'tonumber', 'ipairs', 'print', + 'tonumber', 'ipairs', 'print', 'assert', 'config', - 'real_path', 'C', - 'err_print', 'parse_zip', + 'deb_print', 'err_print', 'parse_zip', }) ----------------------------- view a document ------------------------------ @@ -21,7 +20,7 @@ load_env(L, { -- view a document -- see search.tlu for the structure of the argument function view_doc(docfile) - return view_file(real_path(docfile)) + return view_file(docfile.realpath) end -- get viewer and viewer_replacement before calling try_viewing @@ -39,19 +38,21 @@ function view_file (filename) if zipext then local unzip_cmd = config['unzip_'..zipext] if not unzip_cmd then - err_print("No unzip command for ."..zipext..' files, skipping ' - ..filename, 'error') + err_print('error', + "No unzip command for ."..zipext..' files, skipping '..filename) return false end local tmpdir = os.tmpdir("/tmp/texdoc.XXXXXX") if not tmpdir then - err_print('Failed to create tempdir to unzip.', 'error') + err_print('error', 'Failed to create tempdir to unzip.') return false end local basename = string.match(nozipname, '.*/(.*)$') or nozipname local tmpfile = '"'..tmpdir..'/'..basename..'"' - if not os.execute(unzip_cmd..' "'..filename..'">'..tmpfile) then - err_print("Failed to unzip '"..filename.."'", 'error') + local unzip = unzip_cmd..' "'..filename..'">'..tmpfile + deb_print('view', "Unzip command: "..unzip) + if not os.execute(unzip) then + err_print('error', "Failed to unzip '"..filename.."'") os.remove(tmpfile) os.remove(tmpdir) return false @@ -68,16 +69,11 @@ function view_file (filename) -- special case : sty files use txt viewer if viewext == 'sty' then viewext = 'txt' end if not config['viewer_'..viewext] then - err_print ("cannot find a viewer for file\n\t"..filename.. - "\nUsing viewer_txt as a fallback. Set the 'viewer_"..viewext.. - "' variable in texdoc.cnf to avoid this.", "warning") + err_print('warning', + "No viewer_"..viewext.." defined, using viewer_txt.") viewext = 'txt' - if not config['viewer_'..viewext] then - err_print ("text viewer not found. This ".. - "should not happen, sorry. Skipping\n\t"..filename, "error") - return false - end end + assert(config['viewer_'..viewext], 'Internal error: no viewer found.') return try_viewing(config['viewer_'..viewext], viewer_replacement) end @@ -89,9 +85,9 @@ function try_viewing (view_command, viewer_replacement) else view_command = view_command..' '..viewer_replacement end - err_print(view_command, 'debug1') + deb_print('view', 'View comand: '..view_command) if not os.execute(view_command) then - err_print("Failed to execute '"..view_command.."'", "error") + err_print('error', "Failed to execute '"..view_command.."'") return false end return true @@ -100,18 +96,17 @@ end ----------------------------- display results ------------------------------ -- print a list of files (structure: see search.tlu) as a menu --- if showall is false, stops as soon a non-exact match is encountered --- (unimplemented right now, waiting for the scoring routine) -function print_menu(name, docfiles, showall) +-- if showall is false, stops as soon a bad result is encountered +function print_menu(name, doclist, showall) local max_lines = tonumber(config.max_lines) or 20 - if config.interact_switch and docfiles[max_lines+1] then + if config.interact_switch and doclist[max_lines+1] then -- there may be too many lines, count them local n if showall then - n = #docfiles + n = #doclist else n = 0 - while docfiles[n+1] and docfiles[n+1].exact do + while doclist[n+1] and doclist[n+1]:quality() == 'good' do n = n + 1 end end @@ -126,30 +121,30 @@ function print_menu(name, docfiles, showall) end end local i, doc - for i, doc in ipairs (docfiles) do - if (doc.exact == false) and not showall then break end + 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 config.machine_switch == true then - local score = doc.exact and 1 or 0 - print(name, score, real_path(doc)) + print(name, doc.score, doc.realpath) else - print(string.format('%2d %s', i, real_path(doc))) + print(string.format('%2d %s', i, doc.realpath)) end end if config.interact_switch then io.write ("Please enter the number of the file to view, ", "anything else to skip: ") local num = tonumber(io.read('*line')) - if num and docfiles[num] then - view_doc(docfiles[num]) + if num and doclist[num] then + view_doc(doclist[num]) end end end ----------------------- deliver results base on mode ----------------------- -function deliver_results(name, docfiles, many) +function deliver_results(name, doclist, many) -- ensure that results were found or apologize - if not docfiles[1] then + if not doclist[1] or doclist[1]:quality() == 'killed' then if not config.machine_switch then local msg = string.gsub(C.notfound_msg, C.notfound_msg_ph, name) print(msg) -- get rid of gsub's 2nd value @@ -157,21 +152,21 @@ function deliver_results(name, docfiles, many) return end -- shall we show all of them or only the "good" ones? - local showall = (config.mode == 'regex') or (config.mode == 'search') - if not showall and not docfiles[1].exact then + local showall = (config.mode == 'regex') or (config.mode == 'showall') + if not showall and doclist[1]:quality() ~= 'good' then showall = true - err_print ("No exact match, trying full search mode.", "info") + err_print('info', 'No good result found, showing all results.') end -- view result or show menu based on mode and number of results if (config.mode == 'view') - or config.mode == 'mixed' and (not docfiles[2] - or not docfiles[2].exact and not showall) then - view_doc(docfiles[1]) + or config.mode == 'mixed' and (not doclist[2] + or (doclist[2]:quality() ~= 'good' and not showall)) then + view_doc(doclist[1]) else if many and not config.machine_switch then print ("*** Results for: "..name.." ***") end - print_menu(name, docfiles, showall) + print_menu(name, doclist, showall) end end |