summaryrefslogtreecommitdiff
path: root/Data/Libraries/LDoc/ldoc/doc.lua
diff options
context:
space:
mode:
authorchai <chaifix@163.com>2021-10-30 11:42:13 +0800
committerchai <chaifix@163.com>2021-10-30 11:42:13 +0800
commit53364ddc2e09362cb17432abf4fb598557554a9f (patch)
tree8d2deafc82aceb13db31938a2aecc70927fc1457 /Data/Libraries/LDoc/ldoc/doc.lua
parent42ec7286b2d36a9ba22925f816a17cb1cc2aa5ce (diff)
+ LDoc
Diffstat (limited to 'Data/Libraries/LDoc/ldoc/doc.lua')
-rw-r--r--Data/Libraries/LDoc/ldoc/doc.lua1350
1 files changed, 1350 insertions, 0 deletions
diff --git a/Data/Libraries/LDoc/ldoc/doc.lua b/Data/Libraries/LDoc/ldoc/doc.lua
new file mode 100644
index 0000000..c4908c9
--- /dev/null
+++ b/Data/Libraries/LDoc/ldoc/doc.lua
@@ -0,0 +1,1350 @@
+------
+-- 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 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.
+ -- luacheck: push ignore 542
+ 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 _
+ submodule = true
+ this_mod,_ = 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
+ -- luacheck: pop
+ 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
+ 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]
+ 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 {}
+ for m,v in pairs(amod) do
+ local idx = tonumber(v:match('^%$(%d+)'))
+ if idx then
+ v, value = value:match('(%S+)(.*)')
+ end
+ modifiers[m] = v
+ end
+ 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
+
+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 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 fun_ref
+ 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.items.by_name[self.name..'.'..s]
+ if fun_ref then return reference(self.name..'.'..s,self,fun_ref) 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 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