summaryrefslogtreecommitdiff
path: root/support/texdoc/script/texdoclib-score.tlu
diff options
context:
space:
mode:
authorNorbert Preining <norbert@preining.info>2019-09-02 13:46:59 +0900
committerNorbert Preining <norbert@preining.info>2019-09-02 13:46:59 +0900
commite0c6872cf40896c7be36b11dcc744620f10adf1d (patch)
tree60335e10d2f4354b0674ec22d7b53f0f8abee672 /support/texdoc/script/texdoclib-score.tlu
Initial commit
Diffstat (limited to 'support/texdoc/script/texdoclib-score.tlu')
-rw-r--r--support/texdoc/script/texdoclib-score.tlu293
1 files changed, 293 insertions, 0 deletions
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: