------ -- Defining the ldoc document model. local class = require 'pl.class' local utils = require 'pl.utils' local List = require 'pl.List' local Map = require 'pl.Map' local text = require 'pl.text' local doc = {} local global = require 'ldoc.builtin.globals' local tools = require 'ldoc.tools' local split_dotted_name = tools.split_dotted_name local TAG_MULTI,TAG_ID,TAG_SINGLE,TAG_TYPE,TAG_FLAG,TAG_MULTI_LINE = 'M','id','S','T','N','ML' -- these are the basic tags known to ldoc. They come in several varieties: -- - 'M' tags with multiple values like 'param' (TAG_MULTI) -- - 'ML' tags which have a single multi-lined value like 'usage' (TAG_MULTI_LINE) -- - 'id' tags which are identifiers, like 'name' (TAG_ID) -- - 'S' tags with a single value, like 'release' (TAG_SINGLE) -- - 'N' tags which have no associated value, like 'local` (TAG_FLAG) -- - 'T' tags which represent a type, like 'function' (TAG_TYPE) local known_tags = { param = 'M', see = 'M', comment = 'M', usage = 'ML', ['return'] = 'M', field = 'M', author='M',set='M'; class = 'id', name = 'id', pragma = 'id', alias = 'id', copyright = 'S', summary = 'S', description = 'S', release = 'S', license = 'S', fixme = 'S', todo = 'S', warning = 'S', raise = 'S', charset = 'S', within = 'S', ['local'] = 'N', export = 'N', private = 'N', constructor = 'N', static = 'N',include = 'S', -- project-level module = 'T', script = 'T', example = 'T', topic = 'T', submodule='T', classmod='T', file='T', -- module-level ['function'] = 'T', lfunction = 'T', table = 'T', section = 'T', type = 'T', annotation = 'T', factory = 'T'; } known_tags._alias = {} known_tags._project_level = { module = true, script = true, example = true, topic = true, submodule = true, classmod = true, file = true, } known_tags._code_types = { module = true, script = true, classmod = true, } known_tags._presentation_names = { classmod = 'Class', } known_tags._module_info = { 'copyright','release','license','author' } local see_reference_handlers = {} doc.TAG_MULTI,doc.TAG_ID,doc.TAG_SINGLE,doc.TAG_TYPE,doc.TAG_FLAG = TAG_MULTI,TAG_ID,TAG_SINGLE,TAG_TYPE,TAG_FLAG -- add a new tag. function doc.add_tag(tag,type,project_level) if not known_tags[tag] then known_tags[tag] = type known_tags._project_level[tag] = project_level end end function doc.add_custom_see_handler(pat,action) see_reference_handlers[pat] = action end -- add an alias to an existing tag (exposed through ldoc API) function doc.add_alias (a,tag) known_tags._alias[a] = tag end -- get the tag alias value, if it exists. function doc.get_alias(tag) return known_tags._alias[tag] end -- is it a'project level' tag, such as 'module' or 'script'? function doc.project_level(tag) return known_tags._project_level[tag] end -- is it a project level tag containing code? function doc.code_tag (tag) return known_tags._code_types[tag] end -- is it a section tag? function doc.section_tag (tag) return tag == 'section' or doc.class_tag(tag) end -- is it a class tag, like 'type' or 'factory'? function doc.class_tag (tag) return tag == 'type' or tag == 'factory' end -- how the type wants to be formally presented; e.g. 'module' becomes 'Module' -- but 'classmod' will become 'Class' function doc.presentation_name (tag) local name = known_tags._presentation_names[tag] if not name then name = tag:gsub('(%a)(%a*)',function(f,r) return f:upper()..r end) end return name end function doc.module_info_tags () return List.iter(known_tags._module_info) end -- annotation tags can appear anywhere in the code and may contain any of these tags: known_tags._annotation_tags = { fixme = true, todo = true, warning = true } local acount = 1 function doc.expand_annotation_item (tags, last_item) if tags.summary ~= '' or last_item == nil then return false end local item_name = last_item.tags.name for tag, value in pairs(tags) do if known_tags._annotation_tags[tag] then tags.summary = nil tags:add('class','annotation') tags:add('summary',value) tags:add('name',item_name..'-'..tag..acount) acount = acount + 1 return true elseif tags.error and tag == 'return' then last_item:set_tag(tag,value) end end return false end -- we process each file, resulting in a File object, which has a list of Item objects. -- Items can be modules, scripts ('project level') or functions, tables, etc. -- (In the code 'module' refers to any project level tag.) -- When the File object is finalized, we specialize some items as modules which -- are 'container' types containing functions and tables, etc. local File = class() local Item = class() local Module = class(Item) -- a specialized kind of Item doc.File = File doc.Item = Item doc.Module = Module function File:_init(filename) self.filename = filename self.items = List() self.modules = List() self.sections = List() end function File:new_item(tags,line) local item = Item(tags,self,line or 1) self.items:append(item) return item end function File:export_item (name) for item in self.items:iter() do local tags = item.tags if tags.name == name then tags.export = true if tags['local'] then tags['local'] = nil end return end end -- warn if any of these guys are not found, indicating no -- documentation was given. self:warning('no docs '..tools.quote(name)) end local function has_prefix (name,prefix) local i1,i2 = name:find(prefix) return i1 == 1 and i2 == #prefix end local function mod_section_type (this_mod) return this_mod and this_mod.section and this_mod.section.type end function File:find_module_in_files (name) for f in File.list:iter() do for m in f.modules:iter() do if m.name == name then return m,f.filename end end end end local function init_within_section (mod,name) mod.kinds:add_kind(name, name) mod.enclosing_section = mod.section mod.section = nil return name end function File:finish() local this_mod local items = self.items local tagged_inside self.args = self.args or {} for item in items:iter() do if mod_section_type(this_mod) == 'factory' and item.tags then local klass = '@{'..this_mod.section.name..'}' -- Factory constructors return the object type, and methods all have implicit self argument if item.tags.constructor and not item.tags['return'] then item.tags['return'] = List{klass} elseif item.tags.param then item.tags.param:put('self '..klass) end end item:finish() -- the default is not to show local functions in the documentation. if not self.args.all and (item.type=='lfunction' or (item.tags and item.tags['local'])) then -- don't add to the module -- elseif doc.project_level(item.type) then this_mod = item local package,mname,submodule if item.type == 'module' or item.type == 'classmod' then -- if name is 'package.mod', then mod_name is 'mod' package,mname = split_dotted_name(this_mod.name) if self.args.merge then local mod,mf = self:find_module_in_files(item.name) if mod then print('found master module',mf) this_mod = mod if this_mod.section then print '***closing section from master module***' this_mod.section = nil end submodule = true end end elseif item.type == 'submodule' then local mf submodule = true this_mod,mf = self:find_module_in_files(item.name) if this_mod == nil then self:error("'"..item.name.."' not found for submodule") end tagged_inside = tools.this_module_name(self.base,self.filename)..' Functions' this_mod.kinds:add_kind(tagged_inside, tagged_inside) end if not package then mname = this_mod.name package = '' end if not submodule then this_mod.package = package this_mod.mod_name = mname this_mod.kinds = doc.ModuleMap() -- the iterator over the module contents self.modules:append(this_mod) end elseif doc.section_tag(item.type) then local display_name = item.name if display_name == 'end' then this_mod.section = nil else local summary = item.summary:gsub('%.$','') local lookup_name if doc.class_tag(item.type) then display_name = 'Class '..item.name lookup_name = item.name item.module = this_mod this_mod.items.by_name[item.name] = item else display_name = summary lookup_name = summary item.summary = '' end item.display_name = display_name this_mod.section = item -- the purpose of this little hack is to properly distinguish -- between built-in kinds and any user-defined kinds. this_mod.kinds:add_kind(display_name,display_name..' ',nil,item) this_mod.sections:append(item) this_mod.sections.by_name[lookup_name:gsub('%A','_')] = item end else local to_be_removed -- add the item to the module's item list if this_mod then -- new-style modules will have qualified names like 'mod.foo' if item.name == nil then self:error("item's name is nil") end local mod,fname = split_dotted_name(item.name) -- warning for inferred unqualified names in new style modules -- (retired until we handle methods like Set:unset() properly) if not mod and not this_mod.old_style and item.inferred then --item:warning(item.name .. ' is declared in global scope') end -- the function may be qualified with a module alias... local alias = this_mod.tags.alias if (alias and mod == alias) or mod == 'M' or mod == '_M' then mod = this_mod.mod_name end -- if that's the mod_name, then we want to only use 'foo' if mod == this_mod.mod_name and this_mod.tags.pragma ~= 'nostrip' then item.name = fname end if tagged_inside then item.tags.within = tagged_inside end if item.tags.within then init_within_section(this_mod,item.tags.within) end -- right, this item was within a section or a 'class' local section_description local classmod = this_mod.type == 'classmod' if this_mod.section or classmod then local stype local this_section = this_mod.section if this_section then item.section = this_section.display_name stype = this_section.type end -- if it was a class, then if the name is unqualified then it becomes -- 'Class:foo' (unless flagged as being a constructor, static or not a function) if doc.class_tag(stype) or classmod then if not item.name:match '[:%.]' then -- not qualified name! -- a class is either a @type section or a @classmod module. Is this a _method_? local class = classmod and this_mod.name or this_section.name local static = item.tags.constructor or item.tags.static or item.type ~= 'function' -- methods and metamethods go into their own special sections... if classmod and item.type == 'function' then local inferred_section if item.name:match '^__' then inferred_section = 'Metamethods' elseif not static then inferred_section = 'Methods' end if inferred_section then item.tags.within = init_within_section(this_mod,inferred_section) end end -- Whether to use '.' or the language's version of ':' (e.g. \ for Moonscript) item.name = class..(not static and this_mod.file.lang.method_call or '.')..item.name end if stype == 'factory' then if item.tags.private then to_be_removed = true elseif item.type == 'lfunction' then item.type = 'function' end if item.tags.constructor then item.section = item.type end end end if this_section then --section_description = this_section.summary..' '..(this_section.description or '') --this_section.summary = '' elseif item.tags.within then item.section = item.tags.within else if item.type == 'function' or item.type == 'lfunction' then section_description = "Methods" end item.section = item.type end elseif item.tags.within then -- ad-hoc section... item.section = item.tags.within else -- otherwise, just goes into the default sections (Functions,Tables,etc) item.section = item.type; end item.module = this_mod if not to_be_removed then local these_items = this_mod.items these_items.by_name[item.name] = item these_items:append(item) this_mod.kinds:add(item,these_items,section_description) end -- restore current section after a 'within' if this_mod.enclosing_section then this_mod.section = this_mod.enclosing_section this_mod.enclosing_section = nil end else -- must be a free-standing function (sometimes a problem...) end end item.names_hierarchy = require('pl.utils').split( item.name, '[.:]' ) end end -- some serious hackery. We force sections into this 'module', -- and ensure that there is a dummy item so that the section -- is not empty. function File:add_document_section(title) local section = title:gsub('%W','_') self:new_item { name = section, class = 'section', summary = title } self:new_item { name = 'dumbo', class = 'function', } return section end function Item:_init(tags,file,line) self.file = file self.lineno = line self.summary = tags.summary self.description = tags.description tags.summary = nil tags.description = nil self.tags = {} self.formal_args = tags.formal_args tags.formal_args = nil local iter = tags.iter or Map.iter for tag in iter(tags) do self:set_tag(tag,tags[tag]) end end function Item:add_to_description (rest) if type(rest) == 'string' then self.description = (self.description or '') .. rest end end function Item:trailing_warning (kind,tag,rest) if type(rest)=='string' and #rest > 0 then Item.warning(self,kind.." tag: '"..tag..'" has trailing text ; use not_luadoc=true if you want description to continue between tags\n"'..rest..'"') end end local function is_list (l) return getmetatable(l) == List end function Item:set_tag (tag,value) local ttype = known_tags[tag] local args = self.file.args if ttype == TAG_MULTI or ttype == TAG_MULTI_LINE then -- value is always a List! local ovalue = self.tags[tag] if ovalue then -- already defined, must be a list --print(tag,ovalue,value) if is_list(value) then ovalue:extend(value) else ovalue:append(value) end value = ovalue end -- these multiple values are always represented as lists if not is_list(value) then value = List{value} end if ttype ~= TAG_MULTI_LINE and args and args.not_luadoc then local last = value[#value] if type(last) == 'string' and last:match '\n' then local line,rest = last:match('([^\n]+)(.*)') value[#value] = line self:add_to_description(rest) end end self.tags[tag] = value elseif ttype == TAG_ID then local modifiers if type(value) == 'table' then if value.append then -- it was a List! -- such tags are _not_ multiple, e.g. name if tag == 'class' and value:contains 'example' then self:error("cannot use 'example' tag for functions or tables. Use 'usage'") else self:error("'"..tag.."' cannot have multiple values; "..tostring(value)) end end value = value[1] modifiers = value.modifiers end if value == nil then self:error("Tag without value: "..tag) end local id, rest = tools.extract_identifier(value) self.tags[tag] = id if args and args.not_luadoc then self:add_to_description(rest) else self:trailing_warning('id',tag,rest) end elseif ttype == TAG_SINGLE then self.tags[tag] = value elseif ttype == TAG_FLAG then self.tags[tag] = true if args.not_luadoc then self:add_to_description(value) else self:trailing_warning('flag',tag,value) end else Item.warning(self,"unknown tag: '"..tag.."' "..tostring(ttype)) end end -- preliminary processing of tags. We check for any aliases, and for tags -- which represent types. This implements the shortcut notation. function Item.check_tag(tags,tag, value, modifiers) local alias = doc.get_alias(tag) if alias then if type(alias) == 'string' then tag = alias elseif type(alias) == 'table' then --{ tag, value=, modifiers = } local avalue,amod tag, avalue, amod = alias[1],alias.value,alias.modifiers if avalue then value = avalue..' '..value end if amod then modifiers = modifiers or {} local value_tokens = utils.split(value) for m,v in pairs(amod) do local idx = tonumber(v:match('^%$(%d+)')) if idx then v, value = value:match('(%S+)(.*)') -- v = value_tokens[idx] -- value_tokens[idx] = '' end modifiers[m] = v end -- value = table.concat(value_tokens, ' ') end else -- has to be a function that at least returns tag, value return alias(tags,value,modifiers) end end local ttype = known_tags[tag] if ttype == TAG_TYPE then tags:add('class',tag) tag = 'name' end return tag, value, modifiers end -- any tag (except name and classs) may have associated modifiers, -- in the form @tag[m1,...] where m1 is either name1=value1 or name1. -- At this stage, these are encoded -- in the tag value table and need to be extracted. local function extract_value_modifier (p) if type(p)~='table' then return p, { } else return p[1], p.modifiers or { } end end local function extract_tag_modifiers (tags) local modifiers, mods = {} for tag, value in pairs(tags) do if type(value)=='table' and value.append then -- i.e. it is a List! local tmods = {} for i, v in ipairs(value) do v, mods = extract_value_modifier(v) tmods[i] = mods value[i] = v end modifiers[tag] = tmods else value, mods = extract_value_modifier(value) modifiers[tag] = mods tags[tag] = value end end return modifiers end local function read_del (tags,name) local ret = tags[name] tags[name] = nil return ret end local build_arg_list, split_iden -- forward declaration function Item:split_param (line) local name, comment = line:match('%s*([%w_%.:]+)(.*)') if not name then self:error("bad param name format '"..line.."'. Are you missing a parameter name?") end return name, comment end function Item:finish() local tags = self.tags local quote = tools.quote self.name = read_del(tags,'name') self.type = read_del(tags,'class') self.modifiers = extract_tag_modifiers(tags) self.usage = read_del(tags,'usage') tags.see = read_del(tags,'see') if tags.see then tags.see = tools.identifier_list(tags.see) end if self.usage then for i = 1,#self.usage do local usage = self.usage[i]:gsub('^%s*\n','') self.usage[i] = text.dedent(usage) end end if doc.project_level(self.type) then -- we are a module, so become one! self.items = List() self.sections = List() self.items.by_name = {} self.sections.by_name = {} setmetatable(self,Module) elseif not doc.section_tag(self.type) then -- params are either a function's arguments, or a table's fields, etc. if self.type == 'function' then self.parameter = 'param' self.ret = read_del(tags,'return') self.raise = read_del(tags,'raise') if tags['local'] then self.type = 'lfunction' end else self.parameter = 'field' end local field = self.parameter local params = read_del(tags,field) -- use of macros like @string (which is short for '@tparam string') -- can lead to param tags associated with a table. if self.parameter == 'field' and tags.param then local tparams = read_del(tags,'param') if params then params:extend(tparams) List(self.modifiers.field):extend(self.modifiers.param) else params = tparams self.modifiers.field = self.modifiers.param end end local param_names, comments = List(), List() if params then for line in params:iter() do local name, comment = self:split_param(line) param_names:append(name) comments:append(comment) end end self.modifiers['return'] = self.modifiers['return'] or List() self.modifiers[field] = self.modifiers[field] or List() -- we use the formal arguments (if available) as the authoritative list. -- If there are both params and formal args, then they must match; -- (A formal argument of ... may match any number of params at the end, however.) -- If there are formal args and no params, we see if the args have any suitable comments. -- Params may have subfields. local fargs, formal = self.formal_args if fargs then if #param_names == 0 then --docs may be embedded in argument comments; in either case, use formal arg names local ret formal,comments,ret = self:parse_formal_arguments(fargs) if ret and not self.ret then self.ret = ret end elseif #fargs > 0 then -- consistency check! local varargs = fargs[#fargs] == '...' if varargs then table.remove(fargs) end if tags.export then if fargs[1] == 'self' then table.remove(fargs,1) else tags.static = true end end local k = 0 for _,pname in ipairs(param_names) do local _,field = split_iden(pname) if not field then k = k + 1 if k > #fargs then if not varargs then self:warning("extra param with no formal argument: "..quote(pname)) end elseif pname ~= fargs[k] then self:warning("param and formal argument name mismatch: "..quote(pname).." "..quote(fargs[k])) end end end if k < #fargs then for i = k+1,#fargs do if fargs[i] ~= '...' then self:warning("undocumented formal argument: "..quote(fargs[i])) end end end end -- #fargs > 0 -- formal arguments may come with types, inferred by the -- appropriate code in ldoc.lang if fargs.types then self.modifiers[field] = List() for t in fargs.types:iter() do self:add_type(field,t) end if fargs.return_type then if not self.ret then -- type, but no comment; no worries self.ret = List{''} end self.modifiers['return'] = List() self:add_type('return',fargs.return_type) end end end -- fargs -- the comments are associated with each parameter by -- adding name-value pairs to the params list (this is -- also done for any associated modifiers) -- (At this point we patch up any subparameter references) local pmods = self.modifiers[field] local params, fields = List() local original_names = formal and formal or param_names local names = List() self.subparams = {} params.map = {} for i,name in ipairs(original_names) do if type(name) ~= 'string' then self:error("declared table cannot have array entries") end local pname,field = split_iden(name) if field then if not fields then fields = List() self.subparams[pname] = fields end fields:append(name) else names:append(name) params:append(name) fields = nil end params.map[name] = comments[i] if pmods then pmods[name] = pmods[i] end end self.params = params self.args = build_arg_list (names,pmods) end if self.ret then self:build_return_groups() end end function Item:add_type(field,type) self.modifiers[field]:append {type = type} end -- ldoc allows comments in the formal arg list to be used, if they aren't specified with @param -- Further, these comments may start with a type followed by a colon, and are then equivalent -- to a @tparam function Item:parse_argument_comment (comment,field) if comment then comment = comment:gsub('^%-+%s*','') local type,rest = comment:match '([^:]+):(.*)' if type then self:add_type(field,type) comment = rest end end return comment or '' end function Item:parse_formal_arguments (fargs) local formal, comments, ret = List(), List() if fargs.return_comment then local retc = self:parse_argument_comment(fargs.return_comment,'return') ret = List{retc} end for i, name in ipairs(fargs) do formal:append(name) comments:append(self:parse_argument_comment(fargs.comments[name],self.parameter)) end return formal, comments, ret end function split_iden (name) if name == '...' then return name end local pname,field = name:match('(.-)%.(.+)') if not pname then return name else return pname,field end end function build_arg_list (names,pmods) -- build up the string representation of the argument list, -- using any opt and optchain modifiers if present. -- For instance, '(a [, b])' if b is marked as optional -- with @param[opt] b local buffer, npending = { }, 0 local function acc(x) table.insert(buffer, x) end -- a number of trailing [opt]s will be usually converted to [opt],[optchain],... -- *unless* a person uses convert_opt. if pmods and not doc.ldoc.convert_opt then local m = pmods[#names] if m and m.opt then m.optchain = m.opt for i = #names-1,1,-1 do m = pmods[i] if not m or not m.opt then break end m.optchain = m.opt end end end for i = 1, #names do local m = pmods and pmods[i] local opt if m then if not m.optchain then acc ((']'):rep(npending)) npending=0 end opt = m.optchain or m.opt if opt then acc('[') npending=npending+1 end end if i>1 then acc (', ') end acc(names[i]) if opt and opt ~= true then acc('='..opt) end end acc ((']'):rep(npending)) return '('..table.concat(buffer)..')' end ------ retrieving information about parameters ----- -- The template leans on these guys heavily.... function Item:param_modifiers (p) local mods = self.modifiers[self.parameter] if not mods then return '' end return rawget(mods,p) end function Item:type_of_param(p) local mparam = self:param_modifiers(p) return mparam and mparam.type or '' end -- default value for param; if optional but no default, it's just `true`. function Item:default_of_param(p) local m = self:param_modifiers(p) if not m then return nil end local opt = m.optchain or m.opt return opt end function Item:readonly(p) local m = self:param_modifiers(p) if not m then return nil end return m.readonly end function Item:subparam(p) local subp = rawget(self.subparams,p) if subp then return subp,p else return {p},nil end end function Item:display_name_of(p) local pname,field = split_iden(p) if field then return field else return pname end end -------- return values and types ------- function Item:type_of_ret(idx) local rparam = self.modifiers['return'][idx] return rparam and rparam.type or '' end local function integer_keys(t) if type(t) ~= 'table' then return 0 end for k in pairs(t) do local num = tonumber(k) if num then return num end end return 0 end function Item:return_type(r) if not r.type then return '' end return r.type, r.ctypes end local struct_return_type = '*' function Item:build_return_groups() local quote = tools.quote local modifiers = self.modifiers local retmod = modifiers['return'] local groups = List() local lastg, group for i,ret in ipairs(self.ret) do local mods = retmod[i] local g = integer_keys(mods) if g ~= lastg then group = List() group.g = g groups:append(group) lastg = g end --require 'pl.pretty'.dump(ret) if not mods then self:error(quote(self.name)..' had no return?') end group:append({text=ret, type = mods and (mods.type or '') or '',mods = mods}) end -- order by groups to force error groups to the end table.sort(groups,function(g1,g2) return g1.g < g2.g end) self.retgroups = groups --require 'pl.pretty'.dump(groups) -- cool, now see if there are any treturns that have tfields to associate with local fields = self.tags.field if fields then local fcomments = List() for i,f in ipairs(fields) do local name, comment = self:split_param(f) fields[i] = name fcomments[i] = comment end local fmods = modifiers.field for group in groups:iter() do for r in group:iter() do if r.mods and r.mods.type then local ctypes, T = List(), r.mods.type for i,f in ipairs(fields) do if fmods[i][T] then ctypes:append {name=f,type=fmods[i].type,comment=fcomments[i]} end end r.ctypes = ctypes --require 'pl.pretty'.dump(ctypes) end end end end end local ecount = 0 -- this alias macro implements @error. -- Alias macros need to return the same results as Item:check_tags... function doc.error_macro(tags,value,modifiers) local merge_groups = doc.ldoc.merge_error_groups local g = '2' -- our default group id -- Were we given an explicit group modifier? local key = integer_keys(modifiers) if key > 0 then g = tostring(key) else local l = tags:get 'return' if l then -- there were returns already...... -- maximum group of _existing_ error return local grp, lastr = 0 for r in l:iter() do if type(r) == 'table' then local rg = r.modifiers._err if rg then lastr = r grp = math.max(grp,rg) end end end if grp > 0 then -- cool, create new group if not merge_groups then g = tostring(grp+1) else local mods, text, T = lastr.modifiers local new = function(text) return mods._collected..' '..text,{type='string',[T]=true} end if not mods._collected then text = lastr[1] lastr[1] = merge_groups T = '@'..ecount mods.type = T mods._collected = 1 ecount = ecount + 1 tags:add('field',new(text)) else T = mods.type end mods._collected = mods._collected + 1 return 'field',new(value) end end end end tags:add('return','',{[g]=true,type='nil'}) -- note that this 'return' is tagged with _err! return 'return', value, {[g]=true,_err=tonumber(g),type='string'} end ---------- bothering the user -------------------- function Item:warning(msg) local file = self.file and self.file.filename if type(file) == 'table' then require 'pl.pretty'.dump(file); file = '?' end file = file or '?' io.stderr:write(file,':',self.lineno or '1',': ',self.name or '?',': ',msg,'\n') Item.had_warning = true return nil end function Item:error(msg) self:warning(msg) os.exit(1) end Module.warning, Module.error = Item.warning, Item.error -------- Resolving References ----------------- function Module:hunt_for_reference (packmod, modules) local mod_ref local package = self.package or '' repeat -- same package? local nmod = package..'.'..packmod mod_ref = modules.by_name[nmod] if mod_ref then break end -- cool package = split_dotted_name(package) until not package return mod_ref end local err = io.stderr local function custom_see_references (s) for pat, action in pairs(see_reference_handlers) do if s:match(pat) then local label, href = action(s:match(pat)) if not label then print('custom rule failed',s,pat,href) end return {href = href, label = label} end end end local function reference (s, mod_ref, item_ref) local name = item_ref and item_ref.name or '' -- this is deeply hacky; classes have 'Class ' prepended. --~ if item_ref and doc.class_tag(item_ref.type) then --~ name = 'Class_'..name --~ end return {mod = mod_ref, name = name, label=s} end function Module:lookup_class_item (packmod, s) local klass = packmod --"Class_"..packmod local qs = klass..':'..s local klass_section = self.sections.by_name[klass] if not klass_section then return nil end -- no such class for item in self.items:iter() do --print('item',qs,item.name) if s == item.name or qs == item.name then return reference(s,self,item) end end return nil end function Module:process_see_reference (s,modules,istype) if s == nil then return nil end local mod_ref,fun_ref,name,packmod local ref = custom_see_references(s) if ref then return ref end if not s:match '^[%w_%.%:%-]+$' or not s:match '[%w_]$' then return nil, "malformed see reference: '"..s..'"' end -- `istype` means that we are looking up strictly in a _type_ context, so then only -- allow `classmod` module references. local function ismod(item) if item == nil then return false end if not istype then return true else return item.type == 'classmod' end end -- it is _entirely_ possible that someone does not want auto references for standard Lua libraries! local lua_manual_ref local ldoc = tools.item_ldoc(self) if ldoc and ldoc.no_lua_ref then lua_manual_ref = function(s) return false end else lua_manual_ref = global.lua_manual_ref end -- pure C projects use global lookup (no namespaces) if ldoc and ldoc.global_lookup == nil then local using_c = ldoc.parse_extra and ldoc.parse_extra.C ldoc.global_lookup = using_c or false end -- is this a fully qualified module name? local mod_ref = modules.by_name[s] if ismod(mod_ref) then return reference(s, mod_ref,nil) end -- module reference? mod_ref = self:hunt_for_reference(s, modules) if ismod(mod_ref) then return mod_ref end -- method reference? (These are of form CLASS.NAME) fun_ref = self.items.by_name[s] if fun_ref then return reference(s,self,fun_ref) end -- otherwise, start splitting! local packmod,name = split_dotted_name(s) -- e.g. 'pl.utils','split' if packmod then -- qualified name mod_ref = modules.by_name[packmod] -- fully qualified mod name? if not mod_ref then mod_ref = self:hunt_for_reference(packmod, modules) if not mod_ref then local ref = self:lookup_class_item(packmod,s) if ref then return ref end local mod, klass = split_dotted_name(packmod) mod_ref = modules.by_name[mod] if mod_ref then ref = mod_ref:lookup_class_item(klass,name) if ref then return ref end end ref = lua_manual_ref(s) if ref then return ref end return nil,"module not found: "..packmod end end fun_ref = mod_ref:get_fun_ref(name) if fun_ref then return reference(s,mod_ref,fun_ref) else fun_ref = mod_ref.sections.by_name[name] if not fun_ref then return nil,"function or section not found: "..s.." in "..mod_ref.name else return reference(fun_ref.name:gsub('_',' '),mod_ref,fun_ref) end end else -- plain jane name; module in this package, function in this module if ldoc and ldoc.global_lookup then for m in modules:iter() do fun_ref = m:get_fun_ref(s) if fun_ref then return reference(s,m,fun_ref) end end return nil,"function: "..s.." not found globally" end mod_ref = modules.by_name[self.package..'.'..s] if ismod(mod_ref) then return reference(s, mod_ref,nil) end fun_ref = self:get_fun_ref(s) if fun_ref then return reference(s,self,fun_ref) else local ref = lua_manual_ref (s) if ref then return ref end return nil, "function not found: "..s.." in this module" end end end function Module:get_fun_ref(s) local fun_ref = self.items.by_name[s] -- did not get an exact match, so try to match by the unqualified fun name if not fun_ref then local patt = '[.:]'..s..'$' for qname,ref in pairs(self.items.by_name) do if qname:match(patt) then fun_ref = ref break end end end return fun_ref end -- resolving @see references. A word may be either a function in this module, -- or a module in this package. A MOD.NAME reference is within this package. -- Otherwise, the full qualified name must be used. -- First, check whether it is already a fully qualified module name. -- Then split it and see if the module part is a qualified module -- and try look up the name part in that module. -- If this isn't successful then try prepending the current package to the reference, -- and try to to resolve this. function Module:resolve_references(modules) local found = List() -- Resolve see references in item. Can be Module or Item type. local function resolve_item_references(item) local see = item.tags.see if see then -- this guy has @see references item.see = List() for s in see:iter() do local href, err = self:process_see_reference(s,modules) if href then item.see:append (href) found:append{item,s} elseif err then item:warning(err) end end end end resolve_item_references(self); -- Resolve module-level see references. for item in self.items:iter() do resolve_item_references(item); -- Resolve item-level see references. end -- mark as found, so we don't waste time re-searching for f in found:iter() do f[1].tags.see:remove_value(f[2]) end end function Item:dump_tags (taglist) for tag, value in pairs(self.tags) do if not taglist or taglist[tag] then Item.warning(self,tag..' '..tostring(value)) end end end function Module:dump_tags (taglist) Item.dump_tags(self,taglist) for item in self.items:iter() do item:dump_tags(taglist) end end --------- dumping out modules and items ------------- local function dump_tags (tags) if next(tags) then print 'tags:' for tag, value in pairs(tags) do print('\t',tag,value) end end end -- ANSI colour codes for making important stuff BOLD -- (but not on Windows) local bold,default = '\x1B[1m','\x1B[0m' if utils.dir_separator == '\\' then bold,default = '','' end function Module:dump(verbose) if not doc.project_level(self.type) then return end print '----' print(self.type..':',bold..self.name,self.summary..default) if self.description then print(self.description) end dump_tags (self.tags) for item in self.items:iter() do item:dump(verbose) end end -- make a text dump of the contents of this File object. -- The level of detail is controlled by the 'verbose' parameter. -- Primarily intended as a debugging tool. function File:dump(verbose) for mod in self.modules:iter() do mod:dump(verbose) end end function Item:dump(verbose) local tags = self.tags local name = self.name if self.type == 'function' then name = name .. self.args end if verbose then print() io.write(bold) print(self.type,name) io.write(default) print(self.summary) if self.description and self.description:match '%S' then print 'description:' print(self.description) end if self.params and #self.params > 0 then print 'parameters:' for _,p in ipairs(self.params) do print('',p,self.params.map[p]) end end if self.ret and #self.ret > 0 then print 'returns:' for _,r in ipairs(self.ret) do print('',r) end end dump_tags(self.tags) else print('* '..bold..name..default..' - '..self.summary) end end function doc.filter_objects_through_function(filter, module_list) local quit, quote = utils.quit, tools.quote if filter == 'dump' then filter = 'pl.pretty.dump' end local mod,name = tools.split_dotted_name(filter) local ok,P = pcall(require,mod) if not ok then quit("cannot find module "..quote(mod)) end local ok,f = pcall(function() return P[name] end) if not ok or type(f) ~= 'function' then quit("dump module: no function "..quote(name)) end -- clean up some redundant and cyclical references-- module_list.by_name = nil for mod in module_list:iter() do mod.kinds = nil mod.file = mod.file.filename for item in mod.items:iter() do item.module = nil item.file = nil item.formal_args = nil item.tags['return'] = nil item.see = nil end mod.items.by_name = nil end local ok,err = pcall(f,module_list) if not ok then quit("dump failed: "..err) end end return doc