-- scoring functions for texdoc --[[ Copyright 2008, 2009 Manuel Pégourié-Gonnard Distributed under the terms of the GNU GPL version 3 or later. See texdoc.tlu for details. --]] local L = {} load_env(L, { 'export_symbols', 'string', 'table', 'pairs', 'ipairs', 'tostring', 'tonumber', 'assert', 'config', 'parse_zip', 'err_print', 'deb_print', }) -- 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 -- 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 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', 'userguide', '/user_guide', '-guide', '-user', '-man', 'notes', } -- 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 a.score < b.score then return false else a.ext_pos = a.ext_pos or ext_pos(a.name) b.ext_pos = b.ext_pos or ext_pos(b.name) if a.ext_pos < b.ext_pos then 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 end end -- returns the index of the most specific extension of file in ext_list, -- or config.ext_list_max + 1 function ext_pos(file) -- remove zipext if applicable file = parse_zip(file) -- now find the extension local p, e, pos, ext for p, e in ipairs(config.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 (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_doclist', 'docfile_quality', 'confline_to_score', })