diff options
Diffstat (limited to 'Master/texmf-dist/scripts/texdoc/texdoclib-score.tlu')
-rwxr-xr-x | Master/texmf-dist/scripts/texdoc/texdoclib-score.tlu | 293 |
1 files changed, 293 insertions, 0 deletions
diff --git a/Master/texmf-dist/scripts/texdoc/texdoclib-score.tlu b/Master/texmf-dist/scripts/texdoc/texdoclib-score.tlu new file mode 100755 index 00000000000..81e1be2a333 --- /dev/null +++ b/Master/texmf-dist/scripts/texdoc/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: |