1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
|
-- 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.
--]]
-- Load a local environment. See texdoc.tlu for details.
local L = {}
load_env(L, {
'config', 'parse_zip',
})
-- 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
-- scoring is case-insenstitive (patterns are already lowercased)
local name = string.lower(df.shortname)
deb_print('score', 'Start scoring '..df.realpath)
deb_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 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))
-- get score from tlp associations
if score == -10 and df.tlptodoc then score = -2 end
if score == -10 and df.runtodoc then score = -6 end
-- bonus for metadata
if df.details and
not string.find(string.lower(df.details), '^readme') then
score = score + 3
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
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 -10 <= s < 10
function heuristic_score(file, pat)
deb_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
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
-- if extension is bad, score is < 0
local ext = config.ext_list[ext_pos(file)]
if ext and config.badext_list_inv[ext] and score > 0 then
upscore(-1, 'bad extension', true)
end
-- bonus for being in the right directory
if string.find('/'..file, '/'..pat..'/', 1, true) and not slash 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 lexicographically by filename.
-- 4. then by tree.
-- 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
elseif a.ext_pos < b.ext_pos then return true
elseif a.ext_pos > b.ext_pos then return false
elseif a.basename < b.basename then return true
elseif a.basename > b.basename 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 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
-- 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',
'ext_pos',
'confline_to_score',
})
|