Module:Docbunto
Appearance
| Main | Documentation | Usage |
Lua error in package.lua at line 80: module 'Dev:I18n' not found.
--- Docbunto, based on [[w:c:dev:Global_Lua_Modules/Docbunto|the version on the dev wiki]].
--
-- Compared to the version on the dev wiki, this version:
-- * changes documentation output format to always produce valid HTML and allow CSS styling
-- * adds support for object types that link to other modules
-- * adds better support for block documentation comments
-- * fixes a few bugs
--
-- (Note: @export seems buggy in the version on the dev wiki, and may not be
-- completely fixed in this version either.)
--
-- Documentation from the dev wiki:
--
-- Docbunto is an automatic documentation generator for Scribunto modules.
-- The module is based on LuaDoc and LDoc. It produces documentation in
-- the form of MediaWiki markup, using `@tag`-prefixed comments embedded
-- in the source code of a Scribunto module. The taglet parser & doclet
-- renderer Docbunto uses are also publicly exposed to other modules.
--
-- Docbunto code items are introduced by a block comment (`--[[]]--`), an
-- inline comment with three hyphens (`---`), or an inline `@tag` comment.
-- The module can use static code analysis to infer variable names, item
-- privacy (`local` keyword), tables (`{}` constructor) and functions
-- (`function` keyword). MediaWiki and Markdown formatting is supported.
--
-- Items are usually rendered in the order they are defined, if they are
-- public items, or emulated classes extending the Lua primitives. There
-- are many customisation options available to change Docbunto behaviour.
--
-- @module docbunto
-- @alias p
-- @require Module:I18n
-- @require Module:Lexer
-- @require Module:T
-- @require Module:Unindent
-- @require Module:Yesno
-- @image Docbunto.svg
-- @author [[User:8nml|8nml]]
-- @attribution [[github:stevedonovan|@stevedonovan]] ([[github:stevedonovan/LDoc|Github]])
-- @release stable
-- <nowiki>
local p = {}
local lib = require('Module:Feature')
-- CSS classes
local VISIBILITYLEVEL_CLASS = 'docbunto-visibilitylevel'
local ITEM_CLASS = 'docbunto-item'
local SECTION_CLASS = 'docbunto-section'
local ITEM_HEADER_CLASS = 'docbunto-item-header'
local ITEM_BODY_CLASS = 'docbunto-item-body'
local TAGGROUP_CLASS = 'docbunto-taggroup'
local TAGGROUP_HEADER_CLASS = 'docbunto-taggroup-header'
local TAGGROUP_TAG_CLASS = 'docbunto-taggroup-tag'
local TAG_KEYS_CLASS = 'docbunto-tag-keys'
local INTERWIKI = 'w:c:dev:'
local NEEDS_NEWLINE = '^[{|!};:#*=]' -- tables, dl, ul, ol, headings
local function append_description(description, new_line)
new_line = lib.trim(new_line)
local separator
if #description == 0 then
separator = ''
elseif #new_line == 0 or description:find('\n$') or new_line:find(NEEDS_NEWLINE) then
separator = '\n'
else
separator = ' '
end
return description .. separator .. new_line
end
-- code from the dev wiki:
-- (revision: https://dev.fandom.com/wiki/Module:Docbunto?oldid=193112)
-- Module dependencies.
local title = mw.title.getCurrentTitle()
local i18n = require('Dev:I18n').loadMessages('Module:Docbunto/i18n')
local references = mw.loadData('Dev:Docbunto/references')
local lexer = require('Dev:Lexer')
local unindent = require('Dev:Unindent')
local yesno = require('Dev:Yesno')
-- Module variables.
local DEFAULT_TITLE = title.text:gsub('/doc$', '')
local frame, gsub, match
-- Docbunto variables & tag tokens.
local TAG_MULTI = 'M'
local TAG_ID = 'ID'
local TAG_SINGLE = 'S'
local TAG_TYPE = 'T'
local TAG_FLAG = 'N'
local TAG_MULTI_LINE = 'ML'
local TAG_NAMED_MULTI = 'NM'
-- Docbunto processing patterns.
local DOCBUNTO_SUMMARY, DOCBUNTO_TYPE
local DOCBUNTO_TAG, DOCBUNTO_TAG_VALUE, DOCBUNTO_TAG_MOD_VALUE
-- Docbunto private logic.
--- @{string.find} optimisation for @{string} functions.
-- Resets patterns for each documentation build.
-- @function strfind_wrap
-- @param {function} strfunc String library function.
-- @return {function} Function wrapped in @{string.find} check.
-- @local
function strfind_wrap(func)
return function(...)
local arg = {...}
if string.find(arg[1], arg[2]) then
return func(...);
end
end
end
local function isNotEmpty(tbl)
return tbl[1] ~= nil
end
local function count(str, item)
local output = 0
local _, pos = str:find(item, pos, true)
while pos do
output = output + 1
_, pos = str:find(item, pos + 1, true)
end
return output
end
--- Pattern configuration function.
-- Resets patterns for each documentation build.
-- @function configure_patterns
-- @param {table} options Configuration options.
-- @param {boolean} options.colon Colon mode.
-- @local
local function configure_patterns(options)
-- Setup Unicode or ASCII character encoding (optimisation).
gsub = strfind_wrap(
options.unicode
and mw.ustring.gsub
or string.gsub
)
match = strfind_wrap(
options.unicode
and mw.ustring.match
or string.match
)
DOCBUNTO_SUMMARY =
options.iso639_th
and '^[^ ]+'
or
options.unicode
and '^[^.։。।෴۔።]+[.։。।෴۔።]?'
or '^[^.]+%.?'
-- Set up parsing tag patterns with colon mode support.
DOCBUNTO_TAG = options.colon and '^%s*(%w+):' or '^%s*@(%w+)'
DOCBUNTO_TAG_VALUE = DOCBUNTO_TAG .. '(.*)'
DOCBUNTO_TAG_MOD_VALUE = DOCBUNTO_TAG .. '(%b[])(.*)'
DOCBUNTO_TYPE = '^{({*[^}]+}*)}%s*'
end
--- Markdown header converter.
-- @function markdown_header
-- @param {string} hash Leading hash.
-- @param {string} text Header text.
-- @return {string} MediaWiki header.
-- @local
local function markdown_header(hash, text)
local symbol = '='
return
'\n' .. symbol:rep(#hash) ..
' ' .. text ..
' ' .. symbol:rep(#hash) ..
'\n'
end
--- Item reference formatting.
-- @function item_reference
-- @param {string} ref Item reference.
-- @return {string} Internal MediaWiki link to article item.
-- @local
local function item_reference(ref)
local temp = lib.split(ref, '|')
local item = temp[1]
local text = temp[2] or temp[1]
if references.items[item] then
item = INTERWIKI .. references.items[item]
elseif item:find('#', 1, true) then
item = mw.site.namespaces[828].name .. ':' .. item
else
item = '#' .. item
end
return '<code>' .. '[[' .. item .. '|' .. text .. ']]' .. '</code>'
end
--- Markdown preprocessor to MediaWiki format.
-- @function markdown
-- @param[opt] {string} str Unprocessed Markdown string.
-- @return[opt] {string} MediaWiki-compatible markup with HTML formatting (or nil if `str` is nil).
-- @local
local function markdown(str)
if not str then return nil end
if type(str) ~= "string" then
str = tostring(str)
end
-- Bold & italic tags.
str = str:gsub('%*%*%*([^\n*]+)%*%*%*', '<b><i>%1<i></b>')
str = str:gsub('%*%*([^\n*]+)%*%*', '<b>%1</b>')
str = str:gsub('%*([^\n*]+)%*', '<i>%1</i>')
-- Self-closing header support.
str = str:gsub('%f[^\n%z](#+) *([^\n#]+) *#+%s', markdown_header)
-- External and internal links.
str = str:gsub('%[([^\n%]]+)%]%(([^\n][^\n)]-)%)', '[%2 %1]')
str = str:gsub('%@{([^\n}]+)}', item_reference)
-- Programming & scientific notation.
--'<code><nowiki>%1</nowiki></code>'
str = str:gsub('%f["`]`([^\n`]+)`%f[^"`]', function(m) return '<code>' .. mw.text.nowiki(m) .. '</code>' end)
-- '<chem>%1</chem>'
str = str:gsub('%$%$\\ce{([^\n}]+)}%$%$', function(m) return frame:extensionTag('chem', m) end)
-- '<math display="inline">%1</math>'
str = str:gsub('%$%$([^\n$]+)%$%$', function(m) return frame:extensionTag('math', m, {display='inline'}) end)
-- Strikethroughs and superscripts.
-- (These must be processed after math/chem/code syntax.)
str = str:gsub('~~([^\n~]+)~~', '<del>%1</del>')
str = str:gsub('%^%(([^)]+)%)', '<sup>%1</sup>')
str = str:gsub('%^%s*([^%s%p]+)', '<sup>%1</sup>')
return str
end
local maybe_md
--- Enables/disables markdown processing for each documentation build.
-- @param {table} options Configuration options.
-- @local
function configure_markdown(options)
maybe_md = options.plain and (function(str) return str and tostring(str) or nil end) or markdown
end
--- Tag processor function.
-- @function process_tag
-- @param {string} str Tag string to process.
-- @param[opt] {number} line_no The source line number that defines the tag.
-- @return {table} Tag object.
-- @local
local function process_tag(str, line_no)
local tag = {
lineno=line_no,
height=count(str, '\n') + 1
}
if str:find(DOCBUNTO_TAG_MOD_VALUE) then
tag.category, tag.modifiers, tag.value = str:match(DOCBUNTO_TAG_MOD_VALUE)
local modifiers = {}
-- remove [] from tag.modifiers before splitting
for mod in lib.gsplit(tag.modifiers:sub(2, -2), ',') do
local key, val = mod:match('^(.-)%s*=%s*(.*)$')
if key then
modifiers[key] = val
else
modifiers[mod] = true
end
end
if modifiers.optchain then
modifiers.opt = modifiers.opt or true
modifiers.optchain = nil
end
tag.modifiers = modifiers
else
tag.category, tag.value = str:match(DOCBUNTO_TAG_VALUE)
tag.modifiers = {}
end
tag.value = lib.trim(tag.value)
if p.tags._type_alias[tag.category] then
if p.tags._type_alias[tag.category] ~= 'variable' then
tag.value = p.tags._type_alias[tag.category] .. ' ' .. tag.value
tag.category = 'field'
end
if tag.value:match('^%S+') ~= '...' then
tag.value = tag.value:gsub('^(%S+)', '{%1}')
end
end
tag.category = p.tags._alias[tag.category] or tag.category
if tag.category ~= 'usage' and tag.value:find(DOCBUNTO_TYPE) then
tag.type = tag.value:match(DOCBUNTO_TYPE)
if tag.type:find('^%?') then
tag.type = tag.type:sub(2) .. '|nil'
end
tag.value = tag.value:gsub(DOCBUNTO_TYPE, '')
end
if p.tags[tag.category] == TAG_FLAG then
tag.value = true
end
return tag
end
--- Module info extraction utility.
-- @function extract_info
-- @param {table} documentation Package doclet info.
-- @return {table} Information name-value map.
-- @local
local function extract_info(documentation)
local info = {}
for _, tag in ipairs(documentation.tags) do
if p.tags._module_info[tag.category] then
if info[tag.category] then
if not info[tag.category]:find('^%* ') then
info[tag.category] = '* ' .. info[tag.category]
end
info[tag.category] = info[tag.category] .. '\n* ' .. tag.value
else
info[tag.category] = tag.value
end
end
end
return info
end
--- Type extraction utility.
-- @function extract_type
-- @param {table} item Item documentation data.
-- @return {string} Item type.
-- @local
local function extract_type(item)
local item_cat, item_type
for _, tag in ipairs(item.tags) do
if p.tags[tag.category] == TAG_TYPE then
item_cat = tag.category
item_type = tag.type
break
end
end
return item_cat, item_type
end
--- Name extraction utility.
-- @function extract_name
-- @param {table} item Item documentation data.
-- @param[opt] {string} project_code String containing full project source code.
-- If specified, assumes that `item` is the project and checks
-- its return value when looking for a name.
-- @return {string} Item name.
-- @local
local function extract_name(item, project_code)
opts = opts or {}
local item_name
for _, tag in ipairs(item.tags) do
if p.tags[tag.category] == TAG_TYPE then
item_name = tag.value; break;
end
end
if item_name or not project_code then
return item_name
end
item_name = project_code:match('\nreturn%s+([%w_]+)')
-- TODO why alias as p?
if item_name == 'p' and not item.tags['alias'] then
local implied_alias = { name = 'alias', value = 'p' }
item.tags['alias'] = {implied_alias}
table.insert(item.tags, implied_alias)
end
item_name = (item_name and item_name ~= 'p')
and item_name
or item.filename
:gsub('^' .. mw.site.namespaces[828].name .. ':', '')
:gsub('^(%u)', mw.ustring.lower)
:gsub('/', '.'):gsub(' ', '_')
return item_name
end
--- Source code utility for item name detection.
-- @function deduce_name
-- @param {string} tokens Stream tokens for first line.
-- @param {string} index Stream token index.
-- @param {table} opts Configuration options.
-- @param[opt] {boolean} opts.lookahead Whether a variable name succeeds the index.
-- @param[opt] {boolean} opts.lookbehind Whether a variable name precedes the index.
-- @return {string} Item name.
-- @local
local function deduce_name(tokens, index, opts) -- TODO this should be two different functions
local name = ''
if opts.lookbehind then
for i2 = index, 1, -1 do
if tokens[i2].type ~= 'keyword' then
name = tokens[i2].data .. name
else
break
end
end
elseif opts.lookahead then
for i2 = index, #tokens do
if tokens[i2].type ~= 'keyword' and not tokens[i2].data:find('^%(') then
name = name .. tokens[i2].data
else
break
end
end
end
return name
end
--- Code analysis utility.
-- @function code_static_analysis
-- @param {table} item Item documentation data.
-- @local
local function code_static_analysis(item)
if not item.head then return end
local tokens = item.head
item.head = nil -- remove token data so that logging documentation to console is shorter
local t, i = tokens[1], 1
local item_name, item_cat
local is_local = false
while t do -- TODO this seems unnecessary? just handle it in deduce_name
if t.type == 'whitespace' then
table.remove(tokens, i)
-- skips an index, which is fine since there shouldn't be consecutive whitespace tokens
end
t, i = tokens[i + 1], i + 1
end
t, i = tokens[1], 1
while t do
if t.data == '=' then
item_name = deduce_name(tokens, i - 1, { lookbehind = true })
end
if t.data == 'function' then
item_cat = 'function'
if not tokens[i + 1].data:find('^%(') then
item_name = deduce_name(tokens, i + 1, { lookahead = true })
end
break
end
if t.data:find('^{') then
item_cat = 'table'
break
end
if t.data == 'local' then
is_local = true
end
t, i = tokens[i + 1], i + 1
end
item.name = item.name or item_name or ''
item.category = item.category or item_cat
if item_name or item_cat then
item.has_source = true
end
item.implied_local = is_local
end
local function postprocess_tag(tag)
if p.tags[tag.category] == TAG_NAMED_MULTI then
tag.name = tag.value:match('^%S+')
if tag.name then
tag.value = tag.value:sub(#tag.name + 1)
else
tag.name = '?'
end
end
if type(tag.modifiers.name) == 'string' then
tag.name = tag.modifiers.name
end
-- trim tag value (except flag tags)
if type(tag.value) == "string" then
tag.value = lib.trim(tag.value)
end
return tag
end
--- Partitions tags into sub-lists based on category and runs post-processing
-- on tag values.
-- @function postprocess_tags
-- @param {table} item Item documentation data array.
-- @local
local function postprocess_tags(tags)
for _, element in ipairs(tags) do
if not tags[element.category] then
tags[element.category] = {}
end
table.insert(tags[element.category], postprocess_tag(element))
end
end
--- Item export utility.
-- @function export_item
-- @param {table} documentation Package documentation data.
-- @param {string} item_reference Identifier name for item.
-- @param {string} item_index Identifier name for item.
-- @param {string} item_alias Export alias for item.
-- @param {boolean} factory_item Whether the documentation item is a factory function.
-- @local
local function export_item(documentation, item_reference, item_index, item_alias, factory_item)
-- TODO what if exported multiple times?
documentation.explicitly_exported[item_reference] = {
parent_index = factory_item and item_index or 0,
alias = item_alias
}
end
--- Subitem tag correction utility.
-- @function correct_subitem_tag
-- @param {table} item Item documentation data.
-- @local
local function correct_subitem_tag(item)
local field_tags = item.tags['field']
if item.category ~= 'function' or not field_tags then
return
end
item.tags['param'] = item.tags['param'] or {}
local param_tags = item.tags['param']
for _, tag_el in ipairs(field_tags) do
tag_el.category = 'param'
table.insert(param_tags, tag_el)
end
item.tags['field'] = nil
end
--- Item override tag utility.
-- @function override_item_tag
-- @param {table} item Item documentation data.
-- @param {string} name Tag name.
-- @param[opt] {string} target Target property name for tag.
-- @local
local function override_item_tag(item, name, target)
if item.tags[name] then
item[target or name] = item.tags[name][1].value
end
end
--- Doclet type reference preprocessor.
-- Formats types with links to the [[w:c:dev:Lua reference manual|Lua reference manual]] or wiki pages.
-- @function preop_type
-- @param {table} item Item documentation data.
-- @param {table} options Configuration options.
-- @local
local function type_reference(item, options)
if not options.noluaref and item.name == '...' then
item.name = '[[' .. INTERWIKI .. 'Lua reference manual#varargs|...]]'
end
if not item.type then
return
end
item.type = item.type:gsub(' ', '\26')
local spaces = '%s\26'
local separators = '[;|]' -- TODO should this allow other punctuation? e.g. commas, parens, brackets
local types = mw.text.split(item.type, separators)
for i, type in ipairs(types) do
local t = lib.trim(type, spaces)
local data = references.types[t]
local name = data and data.name or t
if not name:match('%.') and not name:match('^%u') and data then
name = i18n:msg('type-' .. name)
end
if data then
if not options.noluaref then
types[i] = '[[' .. INTERWIKI .. data.link .. '|' .. name .. ']]'
end
else
local link = t
if t:find('#', 1, true) then
link = mw.site.namespaces[828].name .. ':' .. link
else
link = '#' .. link
end
types[i] = '<code>[[' .. link .. '|' .. name .. ']]</code>'
end
end
item.type = mw.text.listToText(types,
i18n:msg('separator-comma'),
#types > 2 and i18n:msg('separator-comma-or') or i18n:msg('separator-or'))
item.type = item.type:gsub('\26', ' ')
end
local function isInstanceFunction(item)
if item.category ~= 'function' then
return false
end
return not item.tags['static']
end
--- Doclet item renderer.
-- @function render_item
-- @param {table} stream Wikitext documentation stream.
-- @param {table} item Item documentation data.
-- @param {table} options Configuration options.
-- @param[opt] {function} preop Item data preprocessor.
-- @return {table} A stream to append children to.
-- @local
local function render_item(stream, item, options, preop)
local item_id = item.alias or item.name or '?'
if preop then preop(item, options) end
local item_name = item.alias or item.name or '?'
type_reference(item, options)
local types = {i18n:msg('itemcategory-' .. item.category)}
for _, name in ipairs(p.tags._subtype_hierarchy) do
if item.tags[name] then
table.insert(types, i18n:msg('itemsubcategory-' .. name))
end
end
table.insert(types, item.type)
local item_cat = i18n:msg('parentheses', table.concat(types, i18n:msg('separator-dot')))
if options.strip and item.export and item.hierarchy then
item_name = item_name:gsub('^[%w_]+[.[]?', '')
end
local source_link = item.has_source
and i18n:msg('parentheses', '[[' .. options.filepath .. '#L-' .. item.lineno .. '|' .. i18n:msg('link-source') .. ']]')
or ''
local dl = stream:tag('dl'):addClass(ITEM_CLASS)
dl:tag('dt')
:addClass(ITEM_HEADER_CLASS)
:wikitext('<code id="', item_id, '">', item_name, '</code>', item_cat, item_typ, source_link)
local dd = dl:tag('dd'):addClass(ITEM_BODY_CLASS):tag('div'):newline()
if #item.description ~= 0 then
dd:wikitext(maybe_md(item.description)):newline()
end
stream:newline()
return dd
end
--- Doclet tag renderer.
-- @function render_tag
-- @param {table} stream Wikitext documentation stream.
-- @param {string} name Item tag name.
-- @param {table} tag Item tag data.
-- @param {table} options Configuration options.
-- @param[opt] {function} preop Item data preprocessor.
-- @local
local function render_tag(stream, name, tag, options, preop)
if preop then preop(tag, options) end
local tag_name = i18n:msg('tag-' .. name, tostring(#tag))
local dl = stream:tag('dl'):addClass(TAGGROUP_CLASS)
dl:tag('dt')
:addClass(TAGGROUP_HEADER_CLASS)
:wikitext('<b>', tag_name, '</b>', i18n:msg('separator-semicolon'))
local nesting = {dl}
for _, tag_el in ipairs(tag) do
local nesting_level = tag_el.name
and (
tag_el.name == '...'
and 1
or (select(2, tag_el.name:gsub('[.[]', '')) + 1)
) or 1
-- handle skipped nesting levels
while nesting_level > #nesting do
-- create elements to nest in
local parent = nesting[#nesting]
local newParent = parent:tag('dl'):addClass(TAG_KEYS_CLASS)
newParent:tag('dt'):wikitext(i18n:msg('label-keys'))
table.insert(nesting, newParent)
end
-- handle end of nesting
local first_in_level = nesting_level > 1 -- never first in level on level 1, since the tag type dt already exists
while nesting_level < #nesting do
table.remove(nesting)
first_in_level = false
end
local parent = nesting[#nesting]
-- create new dl if needed
if first_in_level then
parent = parent:tag('dl'):addClass(TAG_KEYS_CLASS)
parent:tag('dt'):wikitext('Keys:')
nesting[#nesting] = parent
end
local dd = parent:tag('dd'):addClass(TAGGROUP_TAG_CLASS):tag('div'):newline()
table.insert(nesting, dd)
type_reference(tag_el, options)
local extra_data = {}
if tag_el.type then
table.insert(extra_data, tag_el.type)
end
if tag_el.modifiers.opt then
table.insert(extra_data, i18n:msg('optional'))
end
if tag_el.source then
table.insert(extra_data, i18n:msg('line-number', tag_el.source))
end
if isNotEmpty(extra_data) then
dd:wikitext(i18n:msg('parentheses', table.concat(extra_data, i18n:msg('separator-colon'))))
end
dd:newline()
if tag_el.name then
dd:wikitext('<code>', tag_el.name, '</code>')
if #tag_el.value ~= 0 then
dd:wikitext(' ')
end
end
dd:wikitext(maybe_md(tag_el.value)):newline()
end
end
--- Doclet function preprocessor.
-- Formats item name as a function call with top-level arguments.
-- @function preop_function_name
-- @param {table} item Item documentation data.
-- @param {table} options Configuration options.
-- @local
local function preop_function_name(item, options)
local target = item.alias and 'alias' or 'name'
item[target] = item[target] .. '('
if item.tags['param'] then
for index, tag in ipairs(item.tags['param']) do
if not tag.name:find('[.[]') then
item[target] = item[target] .. (index > 1 and ', ' or '')
if tag.modifiers.opt then
item[target] = item[target] .. '<span style="opacity: 0.65;">['
end
item[target] = item[target] .. tag.name
if tag.modifiers.opt then
item[target] = item[target] .. ']</span>'
end
end
end
end
item[target] = item[target] .. ')'
end
--- Doclet usage subitem preprocessor.
-- Formats usage example with `<syntaxhighlight>` tag.
-- @function preop_usage_highlight
-- @param {table} item Item documentation data.
-- @param {table} options Configuration options.
-- @local
local function preop_usage_highlight(items, options)
for _, item in ipairs(items) do
item.value = unindent(lib.trim(item.value))
local multi_line = item.value:find('\n')
local wikitext = item.value:find('^{{.+}}$')
item.value = frame:extensionTag('syntaxhighlight', item.value, {
lang=(wikitext and 'text' or 'lua'),
inline=(multi_line and nil or 1),
})
end
end
--- Doclet error subitem preprocessor.
-- Formats line numbers (`{#}`) in error tag values.
-- @function preop_error_line
-- @param {table} item Item documentation data.
local function preop_error_line(items, options)
for _, item in ipairs(items) do
local line = item.modifiers.line or item.lineno + item.height
if line ~= '' then
item.source = line
end
end
end
-- Docbunto package items.
--- Template entrypoint for [[Template:Docbunto]].
-- @function p.main
-- @param {table} f Scribunto frame object.
-- @return {string} Module documentation output.
function p.main(f)
local args = require('Module:Arguments').getArgs(f)
return p.build(args)
end
--- Scribunto documentation generator entrypoint.
-- @function p.build
-- @param[opt] {table} options Configuration options.
-- @param[opt] {string} options.file Module page name (without namespace).
-- Default: second-level subpage.
-- @param[opt] {string} options.content An parameter used for debugging
-- this module. Pass in a string containing source code
-- to view the documentation it would generate.
-- @param[opt] {boolean} options.all Include local items in
-- documentation.
-- @param[opt] {boolean} options.boilerplate Removal of
-- boilerplate (license block comments).
-- @param[opt] {string} options.caption Infobox image caption.
-- @param[opt] {boolean} options.code Only document Docbunto code
-- items - exclude article infobox and lede from
-- rendered documentation. Permits article to be
-- edited in VisualEditor.
-- @param[opt] {boolean} options.colon Format tags with a `:` suffix
-- and without the `@` prefix. This bypasses the "doctag
-- soup" some authors complain of.
-- @param[opt] {string} options.image Infobox image.
-- @param[opt] {boolean} options.noluaref Don't link to the
-- [[w:c:dev:Lua reference manual|Lua reference manual]]
-- for types.
-- @param[opt] {boolean} options.plain Disable Markdown formatting
-- in documentation.
-- @param[opt] {string} options.preface Preface text to insert
-- between lede & item documentation, used to provide
-- usage and code examples.
-- @param[opt] {boolean} options.simple Limit documentation to
-- descriptions only. Removes documentation of
-- subitem tags such as `@param` and `@field` ([[#Item
-- subtags|see list]]).
-- @param[opt] {boolean} options.strip Remove table index in
-- documentation.
function p.build(args)
local options = {}
options.file = args.file or args[1] or DEFAULT_TITLE
options.content = args.content
options.all = yesno(args.all, false)
options.boilerplate = yesno(args.boilerplate, false)
options.caption = args.caption
options.code = yesno(args.code, false)
options.colon = yesno(args.colon, false)
options.image = args.image
options.noluaref = yesno(args.noluaref, false)
options.plain = yesno(args.plain, false)
options.preface = args.preface
options.simple = yesno(args.simple, false)
options.strip = yesno(args.strip, false)
local tagdata = p.taglet(options)
local docdata = p.doclet(tagdata, options)
return docdata
end
--- Docbunto taglet parser for Scribunto modules.
-- @function p.taglet
-- @param[opt] {table} options Configuration options.
-- @return {table} Module documentation data.
function p.taglet(options)
options = options or {}
options.file = options.file or DEFAULT_TITLE
options.filepath = options.filepath or (mw.site.namespaces[828].name .. ':' .. options.file)
local filepath = options.filepath
local content
if options.content then
-- unstrip nowiki for js preview
content = lib.unstripNoWiki(options.content)
else
content = mw.title.new(filepath):getContent()
end
-- Content checks.
if not content then
-- @error {string} 'Lua source code not found in $1'
error(i18n:msg('no-content', filepath))
end
if
not content:match('%-%-%-') and
not content:match(options.colon and '%s+%w+:' or '%s+@%w+')
then
-- @error {string} 'documentation markup for Docbunto not found in $1'
error(i18n:msg('no-markup', filepath))
end
-- Remove leading escapes.
content = content:gsub('^%-%-+%s*<[^>]+>\n', '')
-- Remove closing pretty comments.
content = content:gsub('\n%-%-%-%-%-+(\n[^-]+)', '\n-- %1')
-- Remove boilerplate block comments.
if options.boilerplate then
content = content:gsub('^%-%-%[=*%[\n.-\n%-?%-?%]%=*]%-?%-?%s+', '')
content = content:gsub('%s+%-%-%[=*%[\n.-\n%-?%-?%]%=*]%-?%-?$', '')
end
-- Configure patterns for colon mode and Unicode character encoding.
options.unicode = type(content:find('[^%w%c%p%s]+')) == 'number'
options.iso639_th = type(content:find('\224\184[\129-\155]')) == 'number'
configure_patterns(options)
-- Content lexing.
local lines = lexer(content)
local tokens = {}
local dummy_token = {
data = '',
posFirst = 1,
posLast = 1
}
local token_closure = 0
for _, line in ipairs(lines) do
if #line == 0 then
dummy_token.type = token_closure == 0
and 'whitespace'
or tokens[#tokens].type
table.insert(tokens, mw.clone(dummy_token))
else
for _, token in ipairs(line) do
if token.data:find('^%[=*%[$') or token.data:find('^%-%-%[=*%[$') then
token_closure = 1
end
if token.data:find(']=*]') then
token_closure = 0
end
table.insert(tokens, token)
end
end
end
-- Start documentation data.
local documentation = {}
documentation.filename = filepath
documentation.description = ''
-- documentation.comments = {}
documentation.tags = {}
documentation.items = {}
documentation.explicitly_exported = {}
documentation.exported_items = {}
documentation.other_items = {}
documentation.private_items = {}
local line_no = 0
local item_index = 0
-- Taglet tracking variables.
local start_mode = true
local comment_mode = false
local doctag_mode = false
local export_mode = false
local special_tag = false
local detachable_tag = false
local comment_tail = ''
local tag_name = ''
local new_item = false
local new_tag = false
local new_item_code = false
local code_block = false
local pretty_comment = false
local comment_brace = false
local t, i = tokens[1], 1
local status, err = pcall(function()
while t do
-- Taglet variable update.
new_item = t.data:find('^%-%-%-') or t.data:find('^%-%-%[=*%[')
comment_tail = t.data:gsub('^%-%-%[=*%[', ''):gsub('^%-%-+', '')
tag_name = comment_tail:match(DOCBUNTO_TAG)
tag_name = p.tags._alias[tag_name] or tag_name
new_tag = p.tags[tag_name]
pretty_comment =
t.data:find('[^-]+%-%-+%s*$') or -- what is this matching?
t.data:find('</?nowiki>') or
t.data:find('</?pre>')
comment_brace =
t.data:find('^%-%-%[=*%[$') or
t.data:find('^%s*%-%-%]=*%]$') or
t.data:find('^%s*%]=*%]%-%-$%s*') or
t.data:find('^%s*%]=*%]$')
pragma_mode = tag_name == 'pragma'
export_mode = tag_name == 'export'
special_tag = pragma_mode or export_mode
detachable_tag = tag_name == 'error'
local tags -- FIXME storing this as a variable is silly
-- Line counter.
if t.posFirst == 1 then
line_no = line_no + 1
end
-- Data insertion logic.
if t.type == 'comment' then
if new_item then comment_mode = true end
-- Module-level documentation taglet.
if start_mode then
-- table.insert(documentation.comments, t.data)
if comment_mode and not new_tag and not doctag_mode and not comment_brace and not pretty_comment then
documentation.description = append_description(documentation.description, comment_tail)
end
if new_tag and not special_tag then
doctag_mode = true
table.insert(documentation.tags, process_tag(comment_tail, line_no))
elseif doctag_mode and not comment_brace and not pretty_comment then
tags = documentation.tags
if p.tags[tags[#tags].category] == TAG_MULTI or p.tags[tags[#tags].category] == TAG_NAMED_MULTI then
tags[#tags].value = append_description(tags[#tags].value, comment_tail)
elseif p.tags[tags[#tags].category] == TAG_MULTI_LINE then
tags[#tags].value = tags[#tags].value .. '\n' .. comment_tail
end
end
end
-- Documentation item detection.
if not start_mode and (new_item or (new_tag and tokens[i - 1].type ~= 'comment')) and not special_tag and not detachable_tag then
table.insert(documentation.items, {})
item_index = item_index + 1
documentation.items[item_index].lineno = line_no
documentation.items[item_index].head = {} -- first line of tokens
-- documentation.items[item_index].comments = {}
documentation.items[item_index].description = ''
documentation.items[item_index].tags = {}
end
if not start_mode and comment_mode and not new_tag and not doctag_mode and not comment_brace and not pretty_comment then
documentation.items[item_index].description = append_description(
documentation.items[item_index].description,
comment_tail)
end
if not start_mode and new_tag and not special_tag then
doctag_mode = true
table.insert(documentation.items[item_index].tags, process_tag(comment_tail, line_no))
elseif not start_mode and doctag_mode and not comment_brace and not pretty_comment then
tags = documentation.items[item_index].tags
if p.tags[tags[#tags].category] == TAG_MULTI or p.tags[tags[#tags].category] == TAG_NAMED_MULTI then
tags[#tags].value = append_description(tags[#tags].value, comment_tail)
elseif p.tags[tags[#tags].category] == TAG_MULTI_LINE then
tags[#tags].value = tags[#tags].value .. '\n' .. comment_tail
end
end
-- if not start_mode and (comment_mode or doctag_mode) then
-- table.insert(documentation.items[item_index].comments, t.data)
-- end
-- Export tag support.
-- Note: string-keyed tables are not supported. (E.g., {['my string'] = foo})
if export_mode then
-- assume that an @export tag at the start of the line must be exporting the module
local factory_mode = t.posFirst ~= 1
if factory_mode then -- NOTE this information doesn't seem to be used
documentation.items[item_index].exports = true
else
documentation.exports = true
end
-- Lua language constructs that can contain , ; = tokens
local BLOCKS = {
['if'] = {terminator='end'},
['do'] = {terminator='end'}, -- also handles 'while'
['repeat'] = {terminator='until'}, -- the part after 'until' is an expression, so it can't have , ; = without being another block
['for'] = {terminator='end', replace='do'},
['function'] = {terminator='end'},
['{'] = {terminator='}'},
['('] = {terminator=')'},
}
-- look ahead, collecting all entries in the next "block" (assumed to be a table)
local items = {}
local si = i+1
local st = tokens[si]
local depth = 0
local blocks = {} -- stack of blocks and {} ()
local curr_item = {value={}}
while st do
local subtokens
if st.type == 'symbol' then
-- break up merged symbols into individual characters (e.g., '{}' or '{},')
subtokens = lib.split(st.data, "")
else
subtokens = {st.data}
end
for _, sst in ipairs(subtokens) do
-- Note: can't have parentheses or other "blocks" around the table
if depth == 1 then
-- apparently, semicolons are valid table item separators in Lua
if sst == ',' or sst == ';' or sst == '}' then
table.insert(items, curr_item)
curr_item = {value={}}
elseif sst == '=' then
curr_item.alias = curr_item.value
curr_item.value = {}
elseif st.type == 'ident' or st.type == 'symbol' then
if curr_item.value then
table.insert(curr_item.value, sst)
end
end
end
if depth > 0 and sst == blocks[depth].terminator then
blocks[depth] = nil
depth = depth - 1
if depth <= 0 then
break
end
elseif depth > 0 and sst == blocks[depth].replace then
blocks[depth] = blocks[depth].replace
elseif BLOCKS[sst] then
depth = depth + 1
blocks[depth] = BLOCKS[sst]
if depth > 1 then
-- don't store value if it's a function or table; ideally, we only want identifiers
curr_item.value = nil
end
end
end
st, si = tokens[si + 1], si + 1
end
local sequence_counter = 0
for _, item in ipairs(items) do
local item_reference, item_alias
if item.value then
item_reference = table.concat(item.value)
end
if item.alias then
item_alias = table.concat(item.alias)
else
sequence_counter = sequence_counter + 1
item_alias = '[' .. sequence_counter .. ']'
end
-- item_reference will be nil if it's an inline function or table
item_reference = item_reference or item_alias
if item_reference then
export_item(documentation, item_reference, item_index, item_alias, factory_mode)
end
end
end
-- Pragma tag support.
if pragma_mode then
tags = process_tag(comment_tail, line_no)
options[tags.value] = yesno((next(tags.modifiers)), true)
if options[tags.value] == nil then
options[tags.value] = true
end
end
-- Data insertion logic.
elseif comment_mode or doctag_mode then
if item_index ~= 0 then
new_item_code = true
end
-- Documentation block reset.
start_mode = false
comment_mode = false
doctag_mode = false
export_mode = false
pragma_mode = false
end
-- Save item head for static analysis.
if item_index ~= 0 and not doctag_mode and not comment_mode and new_item_code then
local head = documentation.items[item_index].head
if t.posFirst == 1 and isNotEmpty(head) then
new_item_code = false
else
table.insert(head, t)
end
end
t, i = tokens[i + 1], i + 1
end
documentation.lineno = line_no
-- post-processing
-- Package data post-processing.
postprocess_tags(documentation.tags)
documentation.name = extract_name(documentation, content)
documentation.info = extract_info(documentation)
documentation.category, documentation.type = extract_type(documentation) or 'module'
documentation.description = lib.trim(documentation.description)
if #documentation.description ~= 0 then
documentation.summary = match(documentation.description, DOCBUNTO_SUMMARY)
end
documentation.executable = p.tags._code_types[documentation.category] and true or false
correct_subitem_tag(documentation)
override_item_tag(documentation, 'name')
override_item_tag(documentation, 'alias')
override_item_tag(documentation, 'summary')
override_item_tag(documentation, 'description')
override_item_tag(documentation, 'class', 'category')
-- Item data post-processing.
-- First pass: basic data extraction from tags and static code analysis
for _, item in ipairs(documentation.items) do
postprocess_tags(item.tags)
item.name = extract_name(item)
item.category, item.type = extract_type(item)
item.description = lib.trim(item.description)
if #item.description ~= 0 then
item.summary = match(item.description, DOCBUNTO_SUMMARY)
end
code_static_analysis(item)
end
-- Second pass: explicit exports and implied locals
-- (This loop requires tags to be processed and names to be set for all items before it runs.)
for _, item in ipairs(documentation.items) do
local export_data = documentation.explicitly_exported[item.name]
if export_data then
local parent_index = export_data.parent_index
local item_alias = export_data.alias
if parent_index > 0 then
local parent = documentation.items[parent_index]
item.alias =
parent.tags['factory'][1].value ..
(item_alias:find('^%[') and '' or (isInstanceFunction(item) and ':' or '.')) ..
item_alias
else
item.alias =
((documentation.tags['alias'] or {}).value or documentation.name) ..
(item_alias:find('^%[') and '' or (documentation.category == 'classmod' and isInstanceFunction(item) and ':' or '.')) ..
item_alias
end
-- TODO is hierarchy even necessary? should it always prioritize alias over base name?
item.hierarchy = mw.text.split((item.alias:gsub('["\']?%]', '')), '[.:%[\'""]+')
if item.category == 'variable' then
item.category = 'member'
end
else
-- never imply local for items explicitly exported with @export
if (item.implied_local or item.category == 'variable') and not (
item.tags['private']
or item.tags['local']
or item.category == 'type'
or (item.alias or item.name):find('[.[:]')
-- functions/tables that belong to the module or a type should not be local
) then
local implied_local = process_tag('@local')
table.insert(item.tags, implied_local)
item.tags['local'] = {implied_local}
end
end
end
-- third pass: update package name alias, process override tags (TODO does this really need its own loop?)
local package_name, package_alias
if documentation.tags['alias'] then
package_name = documentation.tags['alias'][1].value
package_alias = package_name
else
package_name = documentation.name
package_alias = 'p'
end
local export_ptn = '^%s([.[])'
for _, item in ipairs(documentation.items) do
if item.name == package_alias or (item.name and item.name:match('^' .. package_alias .. '[.[]')) then
item.alias = item.name:gsub(export_ptn:format(package_alias), documentation.name .. '%1')
end
if
item.name == package_name or
(item.name and item.name:find(export_ptn:format(package_name))) or
(item.alias and item.alias:find(export_ptn:format(package_name)))
then
item.export = true -- TODO merge into previous if check?
end
if item.name and (item.name:find('[.:]') or item.name:find('%[[\'"]')) then
item.hierarchy = mw.text.split((item.name:gsub('["\']?%]', '')), '[.:%[\'""]+')
end
item.category = item.category or ((item.alias or item.name or ''):find('[.[]') and 'member' or 'variable')
correct_subitem_tag(item)
override_item_tag(item, 'name')
override_item_tag(item, 'alias')
override_item_tag(item, 'summary')
override_item_tag(item, 'description')
override_item_tag(item, 'class', 'category')
end
-- Categorize items: exported, other, or private (local)
for _, item in ipairs(documentation.items) do
if item.category == 'section' then
-- sections default to exported, or private if explicitly marked as such.
-- (sections are never "other.")
if item.tags['local'] or item.tags['private'] then
table.insert(documentation.private_items, item)
else
table.insert(documentation.exported_items, item)
end
else
if item.export then
table.insert(documentation.exported_items, item)
elseif item.tags['local'] or item.tags['private'] then
table.insert(documentation.private_items, item)
else
table.insert(documentation.other_items, item)
end
end
end
end)
if not status then
documentation.error = err
end
return documentation
end
local function render_visibility_level(root, items, options, header_type)
if isNotEmpty(items) then
root = root:tag('div'):addClass(VISIBILITYLEVEL_CLASS):newline()
root:wikitext('=== ' .. i18n:msg(header_type) .. ' ==='):newline()
else
return
end
local codedoc = root -- TODO rename codedoc
for _, item in ipairs(items) do
local itemdoc
if item.category == 'section' then
if item.name == 'end' then
codedoc = root
else
codedoc = root:tag('div'):addClass(SECTION_CLASS):newline()
codedoc:wikitext('==== ', mw.ustring.gsub(item.alias or maybe_md(item.name) or maybe_md(item.summary), '[.։。।෴۔።]$', ''), ' ===='):newline()
if #item.description ~= 0 then
codedoc:wikitext(maybe_md(item.description)):newline()
end
end
elseif item.category == 'type' then
if item.name == 'end' then
codedoc = root
else
codedoc = root:tag('div'):addClass(SECTION_CLASS):newline()
codedoc:wikitext('==== <code>', (item.alias or item.name), '</code> ===='):newline()
if #item.description ~= 0 then
codedoc:wikitext(maybe_md(item.description)):newline()
end
end
elseif item.category == 'function' then
itemdoc = render_item(codedoc, item, options, preop_function_name)
if not options.simple and item.tags['param'] then
render_tag(itemdoc, 'param', item.tags['param'], options)
end
if not options.simple and item.tags['error'] then
render_tag(itemdoc, 'error', item.tags['error'], options, preop_error_line)
end
if not options.simple and item.tags['return'] then
render_tag(itemdoc, 'return', item.tags['return'], options)
end
elseif
item.category == 'table' or item.category == 'member' or item.category == 'variable'
then
itemdoc = render_item(codedoc, item, options)
if not options.simple and item.tags['field'] then
render_tag(itemdoc, 'field', item.tags['field'], options)
end
end
local target = itemdoc or codedoc -- itemdoc doesn't exist for sections (item.category of "section" or "type")
if not options.simple and item.tags['note'] then
render_tag(target, 'note', item.tags['note'], options)
end
if not options.simple and item.tags['warning'] then
render_tag(target, 'warning', item.tags['warning'], options)
end
if not options.simple and item.tags['fixme'] then
render_tag(target, 'fixme', item.tags['fixme'], options)
end
if not options.simple and item.tags['todo'] then
render_tag(target, 'todo', item.tags['todo'], options)
end
if not options.simple and item.tags['usage'] then
render_tag(target, 'usage', item.tags['usage'], options, preop_usage_highlight)
end
if not options.simple and item.tags['see'] then
render_tag(target, 'see', item.tags['see'], options)
end
end
return root
end
--- Doclet renderer for Docbunto taglet data.
-- @function p.doclet
-- @param {table} data Taglet documentation data.
-- @param[opt] {table} options Configuration options.
-- @return {string} Wikitext documentation output.
function p.doclet(data, options)
local documentation = mw.html.create('div'):addClass('docbunto')
if data.error then
documentation:wikitext("Error during documentation parsing: ", data.error):newline():newline()
end
local namespace = '^' .. mw.site.namespaces[828].name .. ':'
local codepage = data.filename:gsub(namespace, '')
options = options or {}
options.filepath = options.filepath or data.filename -- janky way to pass filepath to `render_item`
frame = frame or mw.getCurrentFrame():getParent() or mw.getCurrentFrame() -- parent is nil in dev console
-- Enable/disable markdown based on options
configure_markdown(options)
-- Disable edit sections for automatic documentation pages.
if not options.code then
documentation:wikitext('__NOEDITSECTION__')
end
-- Custom infobox for external wikis.
if not options.code then
local custom, infobox = pcall(require, 'Module:Docbunto/infobox')
if custom and type(infobox) == 'function' then
documentation:wikitext(infobox(data, codepage, frame, options)):newline()
end
end
-- Documentation lede.
if not options.code and #data.description ~= 0 then
local intro = maybe_md(data.description:gsub('^(' .. codepage .. ')', '<b>%1</b>'))
documentation:wikitext(intro):newline():newline()
end
-- Custom documentation preface.
if options.preface then
documentation:wikitext(options.preface):newline():newline()
end
-- Start code documentation.
local function_module = data.tags['param'] or data.tags['return']
if (function_module or isNotEmpty(data.items)) and not options.code or options.preface then
documentation:wikitext('== ', i18n:msg('header-documentation'), ' =='):newline()
end
-- Render documentation items.
if function_module then
data.category = 'function'
if not options.code then data.description = '' end
render_visibility_level(documentation, {data}, options, 'header-function')
else
local header_type = documentation.category == 'classmod' and 'header-class' or 'header-items'
render_visibility_level(documentation, data.exported_items, options, header_type)
end
render_visibility_level(documentation, data.other_items, options, 'header-other')
if options.all then
render_visibility_level(documentation, data.private_items, options, 'header-private')
end
-- Render module-level annotations.
local header_paren = options.code and '===' or '=='
local header_text
for _, tag_name in ipairs{'warning', 'fixme', 'note', 'todo', 'see'} do -- TODO add module-level table with this order; maybe add usage?
if data.tags[tag_name] then
header_text = i18n:msg('tag-' .. tag_name, data.tags[tag_name].value and '1' or '2')
header_text = header_paren .. ' ' .. header_text .. ' ' .. header_paren
documentation:newline():wikitext(header_text):newline()
for _, tag_el in ipairs(data.tags[tag_name]) do
documentation:wikitext('* ', maybe_md(tag_el.value)):newline()
end
end
end
return documentation
end
--- Token dictionary for Docbunto tags.
-- Maps Docbunto tag names to tag tokens.
-- * Multi-line tags use the `'M'` token.
-- * Multi-line preformatted tags use the `'ML'` token.
-- * Identifier tags use the `'ID'` token.
-- * Single-line tags use the `'S'` token.
-- * Flags use the `'N'` token.
-- * Type tags use the `'T'` token.
-- @table p.tags
p.tags = {
-- Item-level tags, available for global use.
['param'] = 'NM', ['see'] = 'M', ['note'] = 'M', ['usage'] = 'ML',
['description'] = 'M', ['field'] = 'NM', ['return'] = 'M',
['fixme'] = 'M', ['todo'] = 'M', ['warning'] = 'M', ['error'] = 'M';
['class'] = 'ID', ['name'] = 'ID', ['alias'] = 'ID';
['summary'] = 'S', ['pragma'] = 'S', ['factory'] = 'S',
['release'] = 'S', ['author'] = 'S', ['copyright'] = 'S', ['license'] = 'S',
['image'] = 'S', ['caption'] = 'S', ['require'] = 'S', ['attribution'] = 'S',
['credit'] = 'S', ['demo'] = 'S';
['local'] = 'N', ['export'] = 'N', ['private'] = 'N', ['constructor'] = 'N',
['static'] = 'N';
-- Project-level tags, all scoped to a file.
['module'] = 'T', ['script'] = 'T', ['classmod'] = 'T', ['topic'] = 'T',
['submodule'] = 'T', ['example'] = 'T', ['file'] = 'T';
-- Module-level tags, used to register module items.
['function'] = 'T', ['table'] = 'T', ['member'] = 'T', ['variable'] = 'T',
['section'] = 'T', ['type'] = 'T';
}
p.tags._alias = {
-- Normal aliases.
['about'] = 'summary',
['abstract'] = 'summary',
['brief'] = 'summary',
['bug'] = 'fixme',
['argument'] = 'param',
['credits'] = 'credit',
['code'] = 'usage',
['details'] = 'description',
['discussion'] = 'description',
['exception'] = 'error',
['lfunction'] = 'function',
['package'] = 'module',
['property'] = 'member',
['raise'] = 'error',
['requires'] = 'require',
['returns'] = 'return',
['throws'] = 'error',
['typedef'] = 'type',
-- Typed aliases.
['bool'] = 'field',
['func'] = 'field',
['int'] = 'field',
['number'] = 'field',
['string'] = 'field',
['tab'] = 'field',
['vararg'] = 'param',
['tfield'] = 'field',
['tparam'] = 'param',
['treturn'] = 'return'
}
p.tags._type_alias = {
-- Implicit type value alias.
['bool'] = 'boolean',
['func'] = 'function',
['int'] = 'number',
['number'] = 'number',
['string'] = 'string',
['tab'] = 'table',
['vararg'] = '...',
-- Pure typed modifier alias.
['tfield'] = 'variable',
['tparam'] = 'variable',
['treturn'] = 'variable'
}
p.tags._project_level = {
-- Contains code.
['module'] = true,
['script'] = true,
['classmod'] = true,
['submodule'] = true,
['file'] = true,
-- Contains documentation.
['topic'] = true,
['example'] = true
}
p.tags._code_types = {
['module'] = true,
['script'] = true,
['classmod'] = true
}
p.tags._module_info = {
['image'] = true,
['caption'] = true,
['release'] = true,
['author'] = true,
['copyright'] = true,
['license'] = true,
['require'] = true,
['credit'] = true,
['attribution'] = true,
['demo'] = true
}
p.tags._annotation_tags = {
['warning'] = true,
['fixme'] = true,
['note'] = true,
['todo'] = true,
['see'] = true
}
p.tags._privacy_tags = {
['private'] = true,
['local'] = true
}
p.tags._generic_tags = {
['variable'] = true,
['member'] = true
}
p.tags._subtype_tags = {
['factory'] = true,
['local'] = true,
['private'] = true,
['constructor'] = true,
['static'] = true
}
p.tags._subtype_hierarchy = {
'private',
'local',
'static',
'factory',
'constructor'
}
return p