summaryrefslogtreecommitdiff
path: root/Master/texmf-dist/scripts/texdoc/search.tlu
diff options
context:
space:
mode:
Diffstat (limited to 'Master/texmf-dist/scripts/texdoc/search.tlu')
-rw-r--r--Master/texmf-dist/scripts/texdoc/search.tlu639
1 files changed, 639 insertions, 0 deletions
diff --git a/Master/texmf-dist/scripts/texdoc/search.tlu b/Master/texmf-dist/scripts/texdoc/search.tlu
new file mode 100644
index 00000000000..1499c65414a
--- /dev/null
+++ b/Master/texmf-dist/scripts/texdoc/search.tlu
@@ -0,0 +1,639 @@
+-- search.tlu: file searching functions for texdoc.
+--
+-- Manuel Pégourié-Gonnard, GPLv3, see texdoclib.tlu for details
+
+-- Warning: every single function here assumes init_databases() has been called.
+
+-- shared by all functions in this file
+local s_doclist -- the Doclist objet to be populated by various functions
+local s_meta -- {[normname] = meta, ...} (populated by init_tlp_database)
+local vanilla -- is this a vanilla TL or a re-package one without tlpdb?
+
+----------------------- docfile and doclist objects ------------------------
+
+--[[
+doclist = {
+ [1] = docfile1, [2] = docfiles2, ...,
+ inv = { realpath1 = index1, ... }
+}
+
+The inv subtable is such that for all i
+doclist.inv(doclist[i].realpath:lower()) == i
+Paths are lowercased in order to avoid duplicates on windows.
+--]]
+
+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)
+ if not df.realpath then return end -- useful if vanilla == false
+ local index = self.inv[df.realpath:lower()]
+ if index then
+ self[index]:mergein(df)
+ else
+ local newindex = #self + 1
+ self[newindex] = df
+ self.inv[df.realpath:lower()] = newindex
+ end
+end
+
+-- stops a doclist
+function Doclist:stop()
+ self.inv = nil
+end
+
+--[[
+docfile = {
+ -- name and tree are mendatory
+ name = filename (used for scoring only)
+ tree = code of the tree, see below
+ -- at least one of the following fields should exist
+ matches = {pattern1, pattern2, ...} or {}
+ runtodoc = true if there is a runfile -> docfile association
+ tlptodoc = true if there is a tlp name -> docfile association
+ -- those are virtual members, see below
+ realpath = full path
+ normname = nomrmalised (path removed up to the 'doc' component)
+ shortname = short name used for scoring
+ basename = basename
+ lang = language tag from the catalogue metadata
+ details = details tag from the catalogue metadata
+ quality = 'good', 'bad', or 'killed' depending on score
+ ext_pos = position of the extension in ext_list
+ -- set for elements of a list as a side effect of sort_doclist()
+ score = score
+}
+if tree > 1, this is the index of the tree in TEXDOCS
+if tree = 0, then name is relative to TLROOT
+tree = - 1 if and only if file is a sty file. Here name is absolute.
+--]]
+
+-- Docfile objects inherit members from Docfile
+-- and have virtual members implemented by get_<member>() methods
+-- for best cache performance, getters should not return nil
+local Docfile = {}
+function Docfile:__index(key)
+ if Docfile[key] then return Docfile[key] end
+ local getter = Docfile['get_'..key]
+ if getter then
+ rawset(self, key, getter(self))
+ return rawget(self, key)
+ end
+end
+
+-- create a new docfile objet using initilisation info
+-- required fields: name, tree
+function Docfile:new(info)
+ local df = {}
+ setmetatable(df, self)
+ for k, v in pairs(info) do
+ if k == 'pattern' then
+ df.matches = { info.pattern }
+ else
+ df[k] = v
+ end
+ end
+ return df
+end
+
+-- merge a second docfile objet in, assuming it represents the same file
+function Docfile:mergein(df)
+ for k, v in pairs(df) do
+ if k == 'matches' then
+ for _, m in ipairs(df.matches or {}) do
+ table.insert(self.matches, m)
+ end
+ else
+ self[k] = v
+ end
+ end
+end
+
+-- return the full path to the file
+function Docfile:get_realpath()
+ if self.tree > 0 then
+ return texdocs_tree_to_path(self.tree, self.name)
+ elseif self.tree == 0 then
+ if vanilla then
+ return get_tlroot()..'/'..self.name
+ else
+ return kpse.find_file(self.normname, 'TeX system documentation')
+ end
+ else
+ return self.name
+ end
+end
+
+-- normalise a name from the tlpdb (use for s_meta indexes)
+function reloc_tlpdb_path(name)
+ return string.gsub(name, '^texmf[^/]*/doc/', '', 1)
+end
+
+-- return normalised name
+function Docfile:get_normname()
+ return (self.tree == 0) and reloc_tlpdb_path(self.name) or self.name
+end
+
+-- retrieve the lang from meta
+function Docfile:get_lang()
+ local meta = s_meta[self.normname]
+ return meta and (meta.lang or false) or false
+end
+
+-- retrieve the details from meta
+function Docfile:get_details()
+ local meta = s_meta[self.normname]
+ return meta and (meta.details or false) or false
+end
+
+-- return the short name used for scoring
+function Docfile:get_shortname()
+ if self.tree == -1 then return self.name end
+ -- remove first component of name if at least two directory levels
+ return string.match(self.normname, '^..-/(.+/.+)$') or self.normname
+end
+
+-- return the base name
+function Docfile:get_basename()
+ return string.gsub(self.name, '.*/', '', 1)
+end
+
+-- for interface consistency, matches should always be a table, never nil
+function Docfile:get_matches()
+ return {}
+end
+
+-- from score.tlu
+Docfile.get_quality = docfile_quality
+
+-- from score.tlu
+function Docfile:get_ext_pos()
+ return ext_pos(self.basename)
+end
+
+--------------------- manage TEXDOCS trees à la kpse ----------------------
+
+do -- scope of doc_roots
+local doc_roots
+
+--[[
+doc_roots[i] = {
+ path = initial path,
+ db = { [file1] = basename1, [file2] = basename2, ... },
+}
+--]]
+
+-- populate the doc_roots filename databases
+function init_texdocs_database()
+ doc_roots = {}
+ local sep = (os.type == 'windows') and ';' or ':'
+ 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)
+ local max = #raw_doc_roots + 1
+ for j, dir in ipairs(raw_doc_roots) do
+ local i = max - j
+ local n
+ local path, db
+ -- get path, !! and // values
+ dir, n = string.gsub (dir, '//$', '')
+ local recursion_allowed = (n == 1)
+ local path, n = string.gsub (dir, '^!!', '')
+ local index_mandatory = (n == 1)
+ deb_print('texdocs', string.format(
+ 'texdocs[%d] = %s (index_mandatory=%s, recursion_allowed=%s)',
+ i, path, tostring(index_mandatory), tostring(recursion_allowed)))
+ -- decide if we should use a ls-R index, the filesystem, or do nothing
+ local root, shift = lsr_root(path)
+ if root and shift and recursion_allowed then
+ deb_print('texdocs', string.format(
+ 'texdocs[%d] using index: %s (shift=%s)', i, root, shift))
+ db = init_lsr_db(root, shift)
+ elseif not index_mandatory and lfs.isdir(path) then
+ deb_print('texdocs', string.format(
+ 'texdocs[%d] using filesystem search', i))
+ db = init_tree_db(path, recursion_allowed)
+ end
+ -- register this in docroots
+ doc_roots[i] = { path = path, db = db }
+ end
+end
+
+-- return the real path from a texdocs tree number + relative path
+function texdocs_tree_to_path(tree, rel)
+ return doc_roots[tree].path..'/'..rel
+end
+
+-- find docfiles in texdocs directories
+function get_doclist_texdocs(patlist)
+ for code, dr in ipairs(doc_roots) do
+ if dr.db then scan_db(patlist, code, dr.db) end
+ end
+end
+
+end -- scope of doc_roots
+
+-- find a ls-R file in a parent directory and return it or nil
+function lsr_root (path)
+ if not lfs.isdir (path) then return end
+ local root, shift = path, ''
+ if string.sub(root, -1) == '/' then root = string.sub(root, 1, -2) end
+ while string.find(root, '/', 1, true) do
+ if lfs.isfile(root..'/ls-R') then
+ return root, shift
+ end
+ local last_comp = string.match(root, '^.*/(.*)$')
+ -- /!\ cannot put last_comp in a regex: can contain special char
+ root = string.sub(root, 1, - (#last_comp + 2))
+ shift = last_comp..'/'..shift
+ end
+end
+
+-- build a db from a ls-R file
+function init_lsr_db(root, shift)
+ -- open the file
+ local lsr = assert(io.open(root..'/ls-R', 'r'))
+ local _ = lsr:read('*line') -- throw away first line (comment)
+ -- scan it
+ local db = {}
+ local maybe_dir, isdoc = true, false
+ local current_dir
+ local l = #shift
+ while true do
+ local line = lsr:read('*line')
+ while line == '' do line, maybe_dir = lsr:read('*line'), true end
+ if line == nil then break end -- EOF
+ local dir_line = maybe_dir and string.match(line, '^%./(.*):$')
+ if dir_line then
+ maybe_dir = false -- next line may not be a dir
+ if string.sub(dir_line..'/', 1, l) == shift then
+ isdoc = true
+ current_dir = string.sub(dir_line, l+1)
+ db[current_dir] = nil
+ elseif isdoc then
+ break -- we're exiting the ./doc (or shift) dir, so it's over
+ end
+ elseif isdoc then
+ local file = (current_dir == '') and line or current_dir..'/'..line
+ if check_ext(line) then db[file] = line end
+ end
+ end
+ lsr:close()
+ return db
+end
+
+-- build a db for a tree without ls-R index
+function init_tree_db(base, recurse)
+ local db = {}
+ local function init_tree_db_rec(dir)
+ for file in lfs.dir(base..'/'..dir) do
+ if file ~= '.' and file ~= '..' then
+ local f = (dir == '') and file or dir..'/'..file
+ if lfs.isdir(base..'/'..f) then
+ if recurse then init_tree_db_rec(f) end
+ else
+ if check_ext(file) then db[f] = file end
+ end
+ end
+ end
+ end
+ init_tree_db_rec('')
+ return db
+end
+
+-------------------- select results from TEXDOCS trees ---------------------
+
+-- scan a database
+function scan_db(patlist, code, lsr_db)
+ for file, basename in pairs(lsr_db) do
+ local df = process_file(patlist, basename, file, code)
+ if df then s_doclist:add(df) end
+ end
+end
+
+-- says if file has a known extenstion according to ext_list
+-- (or known basename according to basename_list)
+function check_ext(file)
+ file = string.lower(file)
+ -- remove zipext if applicable
+ file = parse_zip(file)
+ -- then do the normal thing
+ for _, e in ipairs(config.ext_list) do
+ if e == '*' then
+ return true
+ elseif (e == '') then
+ if not string.find(file, '.', 1, true) then
+ return true
+ end
+ else
+ local dot_e = '.'..e
+ if string.sub(file, -string.len(dot_e)) == dot_e then
+ return true
+ end
+ end
+ end
+ -- is the basename good?
+ for _, b in ipairs(config.basename_list) do
+ if file:find('^'..b..'$') or file:find('^'..b..'%.') then
+ return true
+ end
+ end
+ return false
+end
+
+-- say if a file (with its path) matches a pattern
+function matches(pattern, file)
+ if pattern.original then
+ return string.find(file:lower(), pattern.name:lower(), 1, true)
+ else
+ return is_exact(file, pattern.name)
+ end
+end
+
+-- return a docfile object if file "matches", nil ortherwise
+function process_file(patlist, file, pathfile, code)
+ local docfile
+ local pattern
+ for _, pattern in ipairs(patlist) do
+ if matches(pattern, pathfile) 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
+ return docfile
+end
+
+---------------------------- look for sty files ----------------------------
+
+-- 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
+
+-------------------------------- use tlpdb ---------------------------------
+
+-- tlpdb mean TeX Live Package DataBase and tlp means TeX Live Package
+
+-- find the TeX Live root
+function get_tlroot()
+ local tlroot = kpse.var_value('SELFAUTOPARENT')
+ get_tlroot = function() return tlroot end
+ return tlroot
+end
+
+-- return true if cache exists and is newer than original, false otherwise
+function good_cache(cache, ori)
+ local cache_date = lfs.attributes(cache, 'modification')
+ if not cache_date then return false end
+ local ori_date = assert(lfs.attributes(ori, 'modification'))
+ return cache_date > ori_date
+end
+
+-- make sure a given directory exists, or return nil plus an error string
+function mkdir_p(dir)
+ if lfs.isdir(dir) then return true end
+ local parent = path_parent(dir)
+ if parent then
+ local ok, msg = mkdir_p(parent)
+ if not ok then return nil, msg end
+ end
+ return lfs.mkdir(dir)
+end
+
+-- get tlpinfo tables initialised by whatever mean
+function init_tlp_database()
+ local TEXMFVAR = kpse.var_value('TEXMFVAR')
+ local cache_file = TEXMFVAR..'/'..C.cache_name
+ local texlive_tlpdb = get_tlroot()..'/tlpkg/texlive.tlpdb'
+ vanilla = lfs.isfile(texlive_tlpdb)
+ if vanilla then
+ if good_cache(cache_file, texlive_tlpdb) then
+ deb_print('tlpdb', 'Using cached data from '..cache_file)
+ get_tlpinfo_from_cache(cache_file)
+ else
+ deb_print('tlpdb', 'Getting data from tlpdb file '..texlive_tlpdb)
+ get_tlpinfo_from_tlpdb(texlive_tlpdb)
+ deb_print('tlpdb', 'Writing data in cache file '..cache_file)
+ local ok, msg = mkdir_p(path_parent(cache_file))
+ if not ok then
+ err_print('warning',
+ 'Failed to create cache file in '..cache_file..':')
+ err_print('warning', msg)
+ else
+ print_out_tlpinfo(cache_file)
+ end
+ end
+ else
+ deb_print('tlpdb', 'Using shipped tlpdb data.')
+ get_tlpinfo_from_dist()
+ end
+end
+
+do -- begin scope of tlpinfo tables
+local tlp_from_runfile -- { [runfile_basename] = {tlp1 = true, ...}, ... }
+local tlp_doclist -- { [tlp_name] = { relname1, relname2, ...}, ... }
+
+-- populate tlpinfo tables using the given texlive.tlpdb
+function get_tlpinfo_from_tlpdb(filename)
+ s_meta, tlp_from_runfile, tlp_doclist = {}, {}, {}
+ local curr_tlp
+ local state = 'none'
+ for line in io.lines(filename) do
+ if state == 'none' and string.find(line, '^name ') then
+ -- begin a new package
+ curr_tlp = string.lower(string.sub(line, 6, -1))
+ tlp_doclist[curr_tlp] = {}
+ elseif state == 'docfiles' then
+ if not string.find(line, '^ ') then
+ state = 'none'
+ else
+ local file = string.match(line, '^ ([^ ]*)')
+ local meta = string.match(line, '^ [^ ]* (.+)')
+ local basename = string.match(file, '([^/]*)$')
+ if check_ext(basename) then
+ -- we've got a docfile here, add it
+ table.insert(tlp_doclist[curr_tlp], file)
+ if meta then
+ local details = string.match(meta, 'details="([^"]+)"')
+ local lang = string.match(meta, 'language="([^"]+)"')
+ s_meta[reloc_tlpdb_path(file)] = {
+ details = details,
+ lang = lang,
+ }
+ end
+ end
+ end
+ elseif state == 'runfiles' then
+ if not string.find(line, '^ ') then
+ state = 'none'
+ else
+ -- check for interesting runfiles
+ local e = string.sub(line, -4, -1)
+ if e == '.tex' or e == '.sty' or e == '.cls' then
+ local f = string.match(line, '.*/(.*)%.')
+ tlp_from_runfile[f] = tlp_from_runfile[f] or {}
+ tlp_from_runfile[f][curr_tlp] = true
+ end
+ end
+ end
+ -- update state
+ if string.find(line, '^docfiles ') then
+ state = 'docfiles'
+ elseif string.find(line, '^runfiles ') then
+ state = 'runfiles'
+ end
+ end
+ remove_useless_tlp()
+end
+
+-- remove entries for tlp without any docfile
+function remove_useless_tlp()
+ for tlp, doclist in pairs(tlp_doclist) do
+ if #doclist == 0 then tlp_doclist[tlp] = nil end
+ end
+ for runfile, tlp_set in pairs(tlp_from_runfile) do
+ for tlp in pairs(tlp_set) do
+ if not tlp_doclist[tlp] then
+ tlp_from_runfile[runfile][tlp] = nil
+ end
+ end
+ end
+end
+
+-- print out data from tlpdb in dofile()-able form
+function print_out_tlpinfo(filename)
+ local fh = assert(io.open(filename, 'w'))
+ local function printf(s, ...) fh:write(string.format(s, ...)) end
+ -- s_meta
+ printf('local s_meta = {\n')
+ for k, v in pairs(s_meta) do
+ printf(' [%q] = {', k)
+ for i, j in pairs(v) do printf('[%q] = %q, ', i, j) end
+ printf('},\n')
+ end
+ printf('}\n')
+ -- tlp_from_runfile
+ printf('local tlp_from_runfile = {\n')
+ for k, v in pairs(tlp_from_runfile) do
+ printf(' [%q] = {', k)
+ for f in pairs(v) do printf('[%q]=true,', f) end
+ printf('},\n')
+ end
+ printf('}\n')
+ -- tlp_doclist
+ printf('local tlp_doclist = {\n')
+ for k, v in pairs(tlp_doclist) do
+ printf(' [%q] = {\n', k)
+ for _, f in ipairs(v) do printf(' %q,\n', f) end
+ printf(' },\n')
+ end
+ printf('}\n')
+ printf('return s_meta, tlp_from_runfile, tlp_doclist\n')
+ fh:close()
+end
+
+-- get pre-hashed tlpdb info from a cache file
+function get_tlpinfo_from_cache(filename)
+ s_meta, tlp_from_runfile, tlp_doclist = dofile(filename)
+end
+
+-- get pre-hashed tlpdb info from a pseudo-cache file
+function get_tlpinfo_from_dist()
+ local f = assert(kpse.find_file(C.data_tlpdb_name, 'texmfscripts'))
+ s_meta, tlp_from_runfile, tlp_doclist = dofile(f)
+end
+
+-- get docfiles for pattern using specific tlpdb information
+function get_doclist_tlpdb(pattern)
+ -- runfile to tlp to docfile
+ if tlp_from_runfile[pattern] then
+ for tlp in pairs(tlp_from_runfile[pattern]) do
+ for _, file in ipairs(tlp_doclist[tlp]) do
+ s_doclist:add(Docfile:new{
+ name = file,
+ tree = 0,
+ runtodoc = true,
+ })
+ end
+ end
+ end
+ -- tlp name to docfile
+ if tlp_doclist[pattern] then
+ for _, file in ipairs(tlp_doclist[pattern]) do
+ s_doclist:add(Docfile:new{
+ name = file,
+ tree = 0,
+ tlptodoc = true,
+ })
+ end
+ end
+end
+
+end --scope of tlpinfo table
+
+------------------------------ main function -------------------------------
+
+-- initialise the various databases (must be called first)
+function init_databases()
+ init_texdocs_database()
+ init_tlp_database()
+end
+
+-- find docfiles according to pattern
+function get_doclist(pattern, no_alias)
+ -- get patterns (inc. aliases)
+ local normal, sty = normal_vs_sty(get_patterns(pattern, no_alias))
+ -- initialise result list
+ s_doclist = Doclist:new()
+ -- get results
+ get_doclist_sty(sty)
+ get_doclist_texdocs(normal)
+ get_doclist_tlpdb(pattern)
+ -- finally, sort results
+ sort_doclist(s_doclist, pattern)
+ return s_doclist
+end
+
+-- separate sty patterns from the rest
+function normal_vs_sty(list)
+ 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
+
+return {
+ init_databases = init_databases,
+ get_doclist = get_doclist,
+}