diff options
Diffstat (limited to 'Data/Libraries/LDoc/ldoc/tools.lua')
-rw-r--r-- | Data/Libraries/LDoc/ldoc/tools.lua | 542 |
1 files changed, 542 insertions, 0 deletions
diff --git a/Data/Libraries/LDoc/ldoc/tools.lua b/Data/Libraries/LDoc/ldoc/tools.lua new file mode 100644 index 0000000..6918dab --- /dev/null +++ b/Data/Libraries/LDoc/ldoc/tools.lua @@ -0,0 +1,542 @@ +--------- +-- General utility functions for ldoc +-- @module tools + +local class = require 'pl.class' +local List = require 'pl.List' +local path = require 'pl.path' +local utils = require 'pl.utils' +local tablex = require 'pl.tablex' +local stringx = require 'pl.stringx' +local dir = require 'pl.dir' +local tools = {} +local M = tools +local append = table.insert +local lexer = require 'ldoc.lexer' +local quit = utils.quit + +-- at rendering time, can access the ldoc table from any module item, +-- or the item itself if it's a module +function M.item_ldoc (item) + local mod = item and (item.module or item) + return mod and mod.ldoc +end + +-- this constructs an iterator over a list of objects which returns only +-- those objects where a field has a certain value. It's used to iterate +-- only over functions or tables, etc. If the list of item has a module +-- with a context, then use that to pre-sort the fltered items. +-- (something rather similar exists in LuaDoc) +function M.type_iterator (list,field,value) + return function() + local fls = list:filter(function(item) + return item[field] == value + end) + local ldoc = M.item_ldoc(fls[1]) + if ldoc and ldoc.sort then + fls:sort(function(ia,ib) + return ia.name < ib.name + end) + end + return fls:iter() + end +end + +-- KindMap is used to iterate over a set of categories, called _kinds_, +-- and the associated iterator over all items in that category. +-- For instance, a module contains functions, tables, etc and we will +-- want to iterate over these categories in a specified order: +-- +-- for kind, items in module.kinds() do +-- print('kind',kind) +-- for item in items() do print(item.name) end +-- end +-- +-- The kind is typically used as a label or a Title, so for type 'function' the +-- kind is 'Functions' and so on. + +local KindMap = class() +M.KindMap = KindMap + +-- calling a KindMap returns an iterator. This returns the kind, the iterator +-- over the items of that type, and the actual type tag value. +function KindMap:__call () + local i = 1 + local klass = self.klass + return function() + local kind = klass.kinds[i] + if not kind then return nil end -- no more kinds + while not self[kind] do + i = i + 1 + kind = klass.kinds[i] + if not kind then return nil end + end + i = i + 1 + local type = klass.types_by_kind [kind].type + return kind, self[kind], type + end +end + +function KindMap:put_kind_first (kind) + -- find this kind in our kind list + local kinds = self.klass.kinds + local idx = tablex.find(kinds,kind) + -- and swop with the start! + if idx then + kinds[1],kinds[idx] = kinds[idx],kinds[1] + end +end + +function KindMap:type_of (item) + local klass = self.klass + local kind = klass.types_by_tag[item.type] + return klass.types_by_kind [kind] +end + +function KindMap:get_section_description (kind) + return self.klass.descriptions[kind] +end + +function KindMap:get_item (kind) + return self.klass.items_by_kind[kind] +end + +-- called for each new item. It does not actually create separate lists, +-- (although that would not break the interface) but creates iterators +-- for that item type if not already created. +function KindMap:add (item,items,description) + local group = item[self.fieldname] -- which wd be item's type or section + local kname = self.klass.types_by_tag[group] -- the kind name + if not self[kname] then + self[kname] = M.type_iterator (items,self.fieldname,group) + self.klass.descriptions[kname] = description + end + item.kind = kname:lower() +end + +-- KindMap has a 'class constructor' which is used to modify +-- any new base class. +function KindMap._class_init (klass) + klass.kinds = {} -- list in correct order of kinds + klass.types_by_tag = {} -- indexed by tag + klass.types_by_kind = {} -- indexed by kind + klass.descriptions = {} -- optional description for each kind + klass.items_by_kind = {} -- some kinds are items +end + + +function KindMap.add_kind (klass,tag,kind,subnames,item) + if not klass.types_by_kind[kind] then + klass.types_by_tag[tag] = kind + klass.types_by_kind[kind] = {type=tag,subnames=subnames} + if item then + klass.items_by_kind[kind] = item + end + append(klass.kinds,kind) + end +end + +----- some useful utility functions ------ + +function M.module_basepath() + local lpath = List.split(package.path,';') + for p in lpath:iter() do + local p = path.dirname(p) + if path.isabs(p) then + return p + end + end +end + +-- split a qualified name into the module part and the name part, +-- e.g 'pl.utils.split' becomes 'pl.utils' and 'split'. Also +-- must understand colon notation! +function M.split_dotted_name (s) + local s1,s2 = s:match '^(.+)[%.:](.+)$' + if s1 then -- we can split + return s1,s2 + else + return nil + end +--~ local s1,s2 = path.splitext(s) +--~ if s2=='' then return nil +--~ else return s1,s2:sub(2) +--~ end +end + +-- grab lines from a line iterator `iter` until the line matches the pattern. +-- Returns the joined lines and the line, which may be nil if we run out of +-- lines. +function M.grab_while_not(iter,pattern) + local line = iter() + local res = {} + while line and not line:match(pattern) do + append(res,line) + line = iter() + end + res = table.concat(res,'\n') + return res,line +end + + +function M.extract_identifier (value) + return value:match('([%.:%-_%w]+)(.*)$') +end + +function M.identifier_list (ls) + local ns = List() + if type(ls) == 'string' then ls = List{ns} end + for s in ls:iter() do + if s:match ',' then + ns:extend(List.split(s,'[,%s]+')) + else + ns:append(s) + end + end + return ns +end + +function M.strip (s) + return s:gsub('^%s+',''):gsub('%s+$','') +end + +-- Joins strings using a separator. +-- +-- Empty strings and nil arguments are ignored: +-- +-- assert(join('+', 'one', '', 'two', nil, 'three') == 'one+two+three') +-- assert(join(' ', '', '') == '') +-- +-- This is especially useful for the last case demonstrated above, +-- where "conventional" solutions (".." or table.concat) would result +-- in a spurious space. +function M.join(sep, ...) + local contents = {} + for i = 1, select('#', ...) do + local value = select(i, ...) + if value and value ~= "" then + contents[#contents + 1] = value + end + end + return table.concat(contents, sep) +end + +function M.check_directory(d) + if not path.isdir(d) then + if not dir.makepath(d) then + quit("Could not create "..d.." directory") + end + end +end + +function M.check_file (f,original) + if not path.exists(f) or path.getmtime(original) > path.getmtime(f) then + local text,err = utils.readfile(original) + local _ + if text then + _,err = utils.writefile(f,text) + end + if err then + quit("Could not copy "..original.." to "..f) + end + end +end + +function M.writefile(name,text) + local f,err = io.open(name,"wb") +--~ local ok,err = utils.writefile(name,text) + if err then quit(err) end + f:write(text) + f:close() +end + +function M.name_of (lpath) + local _ + lpath,_ = path.splitext(lpath) + return lpath +end + +function M.this_module_name (basename,fname) + if basename == '' then + return M.name_of(fname) + end + basename = path.abspath(basename) + if basename:sub(-1,-1) ~= path.sep then + basename = basename..path.sep + end + local lpath,cnt = fname:gsub('^'..utils.escape(basename),'') + --print('deduce',lpath,cnt,basename) + if cnt ~= 1 then quit("module(...) name deduction failed: base "..basename.." "..fname) end + lpath = lpath:gsub(path.sep,'.') + return (M.name_of(lpath):gsub('%.init$','')) +end + +function M.find_existing_module (name, dname, searchfn) + local fullpath,lua = searchfn(name) + local mod = true + if not fullpath then -- maybe it's a function reference? + -- try again with the module part + local mpath,fname = M.split_dotted_name(name) + if mpath then + fullpath,lua = searchfn(mpath) + else + fullpath = nil + end + if not fullpath then + return nil, "module or function '"..dname.."' not found on module path" + else + mod = fname + end + end + if not lua then return nil, "module '"..name.."' is a binary extension" end + return fullpath, mod +end + +function M.lookup_existing_module_or_function (name, docpath) + -- first look up on the Lua module path + local on_docpath + local fullpath, mod = M.find_existing_module(name,name,path.package_path) + -- no go; but see if we can find it on the doc path + if not fullpath then + fullpath, mod = M.find_existing_module("ldoc.builtin." .. name,name,path.package_path) + on_docpath = true +--~ fullpath, mod = M.find_existing_module(name, function(name) +--~ local fpath = package.searchpath(name,docpath) +--~ return fpath,true -- result must always be 'lua'! +--~ end) + end + return fullpath, mod, on_docpath -- `mod` can be the error message +end + + +--------- lexer tools ----- + +local tnext = lexer.skipws + +local function type_of (tok) return tok and tok[1] or 'end' end +local function value_of (tok) return tok[2] end + +-- This parses Lua formal argument lists. It will return a list of argument +-- names, which also has a comments field, which will contain any commments +-- following the arguments. ldoc will use these in addition to explicit +-- param tags. + +function M.get_parameters (tok,endtoken,delim,lang) + tok = M.space_skip_getter(tok) + local args = List() + args.comments = {} + local ltl,tt = lexer.get_separated_list(tok,endtoken,delim) + + if not ltl or not ltl[1] or #ltl[1] == 0 then return args end -- no arguments + + local strip_comment, extract_arg + + if lang then + strip_comment = utils.bind1(lang.trim_comment,lang) + extract_arg = utils.bind1(lang.extract_arg,lang) + else + strip_comment = function(text) + return text:match("%s*%-%-+%s*(.*)") + end + extract_arg = function(tl,idx) + idx = idx or 1 + local res = value_of(tl[idx]) + if res == '[' then -- we do allow array indices in tables now + res = '['..value_of(tl[idx + 1])..']' + end + return res + end + end + + local function set_comment (idx,tok) + local text = stringx.rstrip(value_of(tok)) + text = strip_comment(text) + local arg = args[idx] + local current_comment = args.comments[arg] + if current_comment then + text = current_comment .. " " .. text + end + args.comments[arg] = text + end + + local function add_arg (tl,idx) + local name, type = extract_arg(tl,idx) + args:append(name) + if type then + if not args.types then args.types = List() end + args.types:append(type) + end + end + + for i = 1,#ltl do + local tl = ltl[i] -- token list for argument + if #tl > 0 then + local j = 1 + if type_of(tl[1]) == 'comment' then + -- the comments for the i-1 th arg are in the i th arg... + if i > 1 then + while type_of(tl[j]) == 'comment' do + set_comment(i-1,tl[j]) + j = j + 1 + end + else -- first comment however is for the function return comment! + args.return_comment = strip_comment(value_of(tl[i])) + j = j + 1 + end + if #tl > 1 then + add_arg(tl,j) + end + else + add_arg(tl,1) + end + if i == #ltl and #tl > 1 then + while j <= #tl and type_of(tl[j]) ~= 'comment' do + j = j + 1 + end + if j > #tl then break end -- was no comments! + while type_of(tl[j]) == 'comment' do + set_comment(i,tl[j]) + j = j + 1 + end + end + else + return nil,"empty argument" + end + end + + -- we had argument comments + -- but the last one may be outside the parens! (Geoff style) + -- (only try this stunt if it's a function parameter list!) + if (not endtoken or endtoken == ')') and (#args > 0 or next(args.comments)) then + local n = #args + local last_arg = args[n] + if not args.comments[last_arg] then + while true do + tt = {tok()} + if type_of(tt) == 'comment' then + set_comment(n,tt) + else + break + end + end + end + end + -- return what token we ended on as well - can be token _past_ ')' + return args,tt[1],tt[2] +end + +-- parse a Lua identifier - contains names separated by . and (optionally) :. +-- Set `colon` to be the secondary separator, '' for none. +function M.get_fun_name (tok,first,colon) + local res = {} + local t,name,sep,_ + colon = colon or ':' + if not first then + t,name = tnext(tok) + else + t,name = 'iden',first + end + if t ~= 'iden' then return nil end + t,sep = tnext(tok) + while sep == '.' or sep == colon do + append(res,name) + append(res,sep) + _,name = tnext(tok) + t,sep = tnext(tok) + end + append(res,name) + return table.concat(res),t,sep +end + +-- space-skipping version of token iterator +function M.space_skip_getter(tok) + return function () + local t,v = tok() + while t and t == 'space' do + t,v = tok() + end + return t,v + end +end + +function M.quote (s) + return "'"..s.."'" +end + +-- The PL Lua lexer does not do block comments +-- when used in line-grabbing mode, so this function grabs each line +-- until we meet the end of the comment +function M.grab_block_comment (v,tok,patt) + local res = {v} + repeat + v = lexer.getline(tok) + if v:match (patt) then break end + append(res,v) + append(res,'\n') + until false + res = table.concat(res) + --print(res) + return 'comment',res +end + +local prel = path.normcase('/[^/]-/%.%.') + + +function M.abspath (f) + local count + local res = path.normcase(path.abspath(f)) + while true do + res,count = res:gsub(prel,'') + if count == 0 then break end + end + return res +end + +function M.getallfiles(root,mask) + local res = List(dir.getallfiles(root,mask)) + res:sort() + return res +end + +function M.expand_file_list (list, mask) + local exclude_list = list.exclude and M.files_from_list(list.exclude, mask) + local files = List() + local function process (f) + f = M.abspath(f) + if not exclude_list or exclude_list and exclude_list:index(f) == nil then + files:append(f) + end + end + for _,f in ipairs(list) do + if path.isdir(f) then + local dfiles = M.getallfiles(f,mask) + for f in dfiles:iter() do + process(f) + end + elseif path.isfile(f) then + process(f) + else + quit("file or directory does not exist: "..M.quote(f)) + end + end + return files +end + +function M.process_file_list (list, mask, operation, ...) + local files = M.expand_file_list(list,mask) + for f in files:iter() do + operation(f,...) + end +end + +function M.files_from_list (list, mask) + local excl = List() + M.process_file_list (list, mask, function(f) + excl:append(f) + end) + return excl +end + + + +return tools |