diff options
Diffstat (limited to 'support/digestif/digestif/data.lua')
-rw-r--r-- | support/digestif/digestif/data.lua | 384 |
1 files changed, 384 insertions, 0 deletions
diff --git a/support/digestif/digestif/data.lua b/support/digestif/digestif/data.lua new file mode 100644 index 0000000000..621048f4a9 --- /dev/null +++ b/support/digestif/digestif/data.lua @@ -0,0 +1,384 @@ +local lpeg = require "lpeg" +local util = require "digestif.util" +local config = require "digestif.config" + +local format, strfind = string.format, string.find +local concat, unpack = table.concat, table.unpack +local popen = io.popen +local P, C, Cg, Ct = lpeg.P, lpeg.C, lpeg.Cg, lpeg.Ct +local match = lpeg.match +local many, sequence = util.many, util.sequence +local gobble, search = util.gobble, util.search +local nested_get, extend = util.nested_get, util.extend +local find_file = util.find_file +local parse_uri, make_uri = util.parse_uri, util.make_uri +local log = util.log +local path_split, path_join = util.path_split, util.path_join + +local data = {} +local loaded_tags = {} + +--* CTAN data + +-- function ctan_package(name) +-- +-- Return a little tags table with the package's details on CTAN +-- (package description, ctan link, documentation). No command +-- information. +-- +-- function ctan_package_of(file) +-- +-- The name of the CTAN package to which file belongs. +-- +local ctan_package, ctan_package_of -- to be defined + +local tlpdb_path = config.tlpdb_path + and find_file(config.tlpdb_path) + or find_file(config.texmf_dirs, "../tlpkg/texlive.tlpdb") + +if tlpdb_path then + + local _, tlpdb_text = find_file(tlpdb_path, nil, true) + + if config.verbose then + log("Reading TLPDB from '%s'", tlpdb_path) + end + + local Peol = P"\n" + local concat_lines = function(...) return concat({...}, "\n") end + + local within_item = 1 - Peol * Peol + local within_files = (1 - Peol) + Peol * P" " + local gobble_to_eol = gobble("\n") + + -- Pattern to turn an entry in the TLPDB file into a table with + -- entries name, summary, details, documentation, runfiles. + local tlpdb_item_patt = Ct( + sequence( + Cg( -- collect the name + P"name " * C(gobble_to_eol), + "ctan_package"), + Cg( -- find a shortdesc or give up + search( + Peol * "shortdesc " * C(gobble_to_eol), + within_item), + "summary"), + Cg( -- find a longdesc or give up + search( + (Peol * "longdesc " * C(gobble_to_eol))^1 / concat_lines, + within_item), + "details"), + Cg( -- find docfiles section + many(-1, -- or continue + sequence( + search(Peol * "docfiles " * gobble_to_eol, within_item), + Ct(many( -- collect several docfile entries in a list + search( -- looking for the interesting ones only + Ct( -- parse one docfile entry + sequence( -- collect URI, but only if details field exist + Peol * " RELOC/", + Cg(gobble(" ", 1 - Peol) / "texmf:%0", "uri"), + Cg(P" details=\"" * C(gobble"\""), "summary"))), + within_files))))), + "documentation"), + Cg( -- find runfiles section or give up + sequence( + search(Peol * "runfiles " * gobble_to_eol, within_item), + Ct(many( -- collect the runfile entries is a list + sequence( -- parse one runfile entry + Peol * " ", -- discard the file line marker + many(search("/", 1 - Peol)), -- discard folder part + C(gobble_to_eol))))), -- collect base name + "runfiles"))) + + local tlpdb_items = Ct(many(search(Peol * tlpdb_item_patt))):match(tlpdb_text) + + function ctan_package(name) + for i = 1, #tlpdb_items do + local item = tlpdb_items[i] + if item.name == name then + return item + end + end + end + ctan_package = util.memoize1(ctan_package) + + function ctan_package_of(file) + for i = 1, #tlpdb_items do + local item = tlpdb_items[i] + local runfiles = item.runfiles + for j = 1, #runfiles do + if runfiles[j] == file then + return item + end + end + end + end + ctan_package_of = util.memoize1(ctan_package_of) + +else + + ctan_package = function() end + ctan_package_of = function() end + if config.verbose then log("TLPDB not found") end + +end + +--* kpathsea emulation + +-- function kpsewhich(name) +-- +-- Like the kpsewhich command, return the full path of tex input file +-- name. We use luatex's kpse bindings if available, and parse the +-- system's ls-R files otherwise. +-- +local kpsewhich + +if kpse then -- we're on luatex + + local kpse_obj = kpse.new("luatex") + + function kpsewhich(name) + return kpse_obj:find_file(name, "tex") + end + +else -- on plain lua, we look for ls-R files + + local texmf_files = {} + local texmf_dirs = config.texmf_dirs + local dir_patt = P"./" * C(gobble(":" * P(-1))) + for i = 1, #texmf_dirs do + local texmf_dir = texmf_dirs[i] + local listing_path = find_file(texmf_dir, "ls-R") + if listing_path then + local current_dir + for line in io.lines(listing_path) do + local subdir = match(dir_patt, line) + if subdir then + current_dir = path_join(texmf_dir, subdir) + elseif current_dir and not texmf_files[line] then + texmf_files[line] = path_join(current_dir, line) + end + end + end + end + + function kpsewhich(name) + return texmf_files[name] + end + +end + +--* Generate tags from the user's TeX installation + +local function infer_format(path) + local ext = path:sub(-4) + if ext == ".bib" then + return "bibtex" + elseif ext == ".sty" or ext == ".cls" or ext == ".ltx" then + return "latex-prog" + elseif ext == ".xml" and path:match("%Acontext%A") then + return "context-xml" + elseif ext == ".tex" then + return "latex-prog" + end +end + +local function tags_from_manuscript(script, ctan_data) + local commands, environments, dependencies = {}, {}, {} + local tags = { + generated = true, + dependencies = dependencies, + commands = commands, + environments = environments + } + if ctan_data then + setmetatable(tags, {__index = ctan_data}) + end + for pkg in pairs(script.packages) do + dependencies[#dependencies+1] = pkg + end + table.sort(dependencies) + for _, it in script:index_pairs("newcommand") do + commands[it.name] = { + arguments = it.arguments, + } + end + for _, it in script:index_pairs("newenvironment") do + environments[it.name] = { + arguments = it.arguments, + } + end + return tags +end + +-- Generate tags from TeX source code (or ConTeXt interface XML file, +-- if applicable). The argument is a file name found by kpsewhich. +-- +local function generate_tags(name) + local path = kpsewhich(name) + path = path and find_file(config.texmf_dirs, path) + if not path then return end + local texformat = infer_format(path) + if not texformat then return end + if config.verbose then log("Generating tags: %s", path) end + local pkg = ctan_package_of(name) + if texformat == "context-xml" then + -- The function below is defined and monkey-patched in + -- ManuscriptConTeXt, to keep the XML dependency separated. + if not data.tags_from_xml then require "digestif.ManuscriptConTeXt" end + return data.tags_from_xml(path, pkg) + else + loaded_tags[name] = {} -- TODO: this is to avoid loops, find a better way + local cache = require "digestif.Cache"() + local script = cache:manuscript(path, texformat) + return tags_from_manuscript(script, pkg) + end +end +data.generate_tags = generate_tags + +--* Load tags + +-- function require_tags(name) +-- +-- Return tags table for input file name. Either loads from a tags +-- file in the data directory, or generate from source on the fly. +-- +local require_tags + +local parse_ref = util.matcher( + sequence( + P"$ref:", + C(gobble"#") * P"#", + Ct(many(P"/" * C(gobble"/"))))) +p=parse_ref +local function resolve_refs(tbl, seen) + seen = seen or {} + for k, v in pairs(tbl) do + if type(v) == "string" then + local loc, path = parse_ref(v) + if loc then + tbl[k] = nested_get(require_tags(loc), unpack(path)) + end + elseif type(v) == "table" and not seen[v] then + seen[v] = true + resolve_refs(v, seen) + end + end +end + +-- Load a tags file from the data directory. +local function load_tags(name) + if strfind(name, "..", 1, true) then return end -- bad file name + local path, str = find_file(config.data_dirs, name .. ".tags", true) + if not path then return end + if config.verbose then log("Loading tags: %s", path) end + local tags = {} + local ok, message = load(str, path, "t", tags) + if ok then ok, message = pcall(ok) end + if not ok and config.verbose then + log("Error loading %s.tags: %s", name, message) + return -- TODO: should throw an error? + end + local pkg = ctan_package(tags.ctan_package) or ctan_package_of(name) + setmetatable(tags, {__index = pkg}) + return tags +end + +require_tags = function(name) + local tags = loaded_tags[name] + if not tags then + tags = load_tags(name) or generate_tags(name) + if tags then + loaded_tags[name] = tags + resolve_refs(tags) + local extra_actions = config.extra_actions or {} + for _, kind in ipairs{"commands", "environments"} do + for cs, cmd in pairs(tags[kind] or {}) do + if not cmd.package then cmd.package = tags end + if extra_actions[cs] then cmd.action = extra_actions[cs] end + end + end + end + end + return tags +end +data.require = require_tags +data.require_tags = require_tags + +-- Load all data files, and return them in a table. This is intended +-- for debugging and testing only, and depends on luafilesystem. +local function load_all_tags() + local t = {} + local ok, lfs = pcall(require, "lfs") + assert(ok, "Function data.load_all() need the luafilesystem library.") + for _, data_dir in ipairs(config.data_dirs) do + for path in lfs.dir(data_dir) do + local pkg = path:match("(.*)%.tags") + if pkg then + assert(load_tags(pkg), "Error loading data file " .. path) + t[pkg] = require_tags(pkg) or error("Error processing data file " .. path) + end + end + end + return t +end +data.load_all = load_all_tags + +--* User-readable documentation + +local function resolve_uri(uri) + local scheme, _, location, _, fragment = parse_uri(uri) + if scheme == "info" then + return uri + elseif scheme == "texmf" then + local path = find_file(config.texmf_dirs, location) + if path then + return make_uri("file", "", path, nil, fragment) + else + return format( + config.external_texmf, make_uri(nil, nil, location, nil, fragment) + ) + end + else + return uri + end +end + +-- Given a list of documentation items, return a markdown-formatted +-- string of links to these documents. +local function resolve_doc_items(items) + if type(items) == "string" then items = {items} end + local t = {} + for _, item in ipairs(items) do + if type(item) == "string" then + t[#t+1] = format("- <%s>", resolve_uri(item)) + else + t[#t+1] = format("- [%s](%s)", item.summary, resolve_uri(item.uri)) + end + end + return t +end +data.resolve_doc_items = resolve_doc_items + +-- Call info, return the relevant part of the info node. +local function get_info(uri) + if config.info_command then + local scheme, _, path, _, fragment = parse_uri(uri) + if scheme ~= "info" then return end + local cmd = format("%s '(%s)%s'", config.info_command, path, fragment) + local pipe = popen(cmd) + local str = pipe:read("a") + local ok, exitt, exitc = pipe:close() + if ok and exitt == "exit" and exitc == 0 then + str = str:gsub(".-\n", "", 2) -- discard header line + str = str:gsub("\nFile: " .. path .. ".info.*", "\n") -- discard child nodes + return str, path, fragment + elseif config.verbose then + log("Error running info (%s %d)", exitt, exitc) + end + end +end +data.get_info=util.memoize(get_info) + +return data |