See Global Lua Modules/Docbunto
- Subpages
- Module:Docbunto/cli
- Module:Docbunto/doc
- Module:Docbunto/i18n
- Module:Docbunto/i18n/doc
- Module:Docbunto/references
- Module:Docbunto/references/doc
- Module:Docbunto/testcases
- Module:Docbunto/testcases/Basic
- Module:Docbunto/testcases/Basic/doc
- Module:Docbunto/testcases/Classmod
- Module:Docbunto/testcases/Classmod/doc
- Module:Docbunto/testcases/Export
- Module:Docbunto/testcases/Export/doc
- Module:Docbunto/testcases/Factory
- Module:Docbunto/testcases/Factory/doc
- Module:Docbunto/testcases/Function
- Module:Docbunto/testcases/Function/doc
- Module:Docbunto/testcases/Generics
- Module:Docbunto/testcases/Generics/doc
- Module:Docbunto/testcases/LDoc
- Module:Docbunto/testcases/LDoc/doc
- Module:Docbunto/testcases/Markdown
- Module:Docbunto/testcases/Markdown/doc
- Module:Docbunto/testcases/Overloads
- Module:Docbunto/testcases/Overloads/doc
- Module:Docbunto/testcases/Topic
- Module:Docbunto/testcases/Topic/doc
- Module:Docbunto/testcases/doc
- Module:Docbunto/topic
- Module:Docbunto/topic/doc
--- 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]]
-- @author [[User:TheReelDevs|TheReelDevs]]
-- @attribution [[github:stevedonovan|@stevedonovan]] ([[github:stevedonovan/LDoc|Github]])
-- @release stable
-- <nowiki>
local p = {}
-- Module dependencies.
local title = mw.title.getCurrentTitle()
local i18n = require('Dev:I18n').loadMessages('Docbunto', 'Infobox'):useContentLang()
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 DEV_WIKI = 'https://dev.fandom.com'
local DEFAULT_TITLE = title.text
:gsub('^Global Lua Modules/', '', 1)
:gsub('/.*', '')
local DEFAULT_VARIABLE = DEFAULT_TITLE
:gsub('^%u', mw.ustring.lower)
:gsub('%s', '_'):gsub('%p', '.')
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'
-- Docbunto processing patterns.
local patterns = {}
--- Configuration options.
-- @table options
-- @field[opt] {boolean} options.all Include local items in
-- documentation.
-- @field[opt] {boolean} options.boilerplate Removal of
-- boilerplate (license block comments).
-- @field[opt] {string} options.caption Infobox image caption.
-- @field[opt] {boolean} options.code Only document Docbunto code
-- items - exclude article infobox and lede from
-- rendered documentation. Permits article to be
-- edited in VisualEditor.
-- @field[opt] {boolean} options.colon Format tags with a `:` suffix
-- and without the `@` prefix. This bypasses the "doctag
-- soup" some authors complain of.
-- @field[opt] {string} options.image Infobox image.
-- @field[opt] {boolean} options.box Include infobox in output.
-- @field[opt] {boolean} options.footer Append a horizontal line &
-- "generated by Docbunto" to the bottom of the output.
-- @field[opt] {boolean} options.noluaref Don't link to the [[Lua
-- reference manual]] for types.
-- @field[opt] {boolean} options.card Include endmatter card in the
-- output at the end of the documentation.
-- @field[opt] {boolean} options.plain Disable Markdown formatting
-- in documentation.
-- @field[opt] {string} options.preface Preface text to insert
-- between lede & item documentation, used to provide
-- usage and code examples.
-- @field[opt] {boolean} options.simple Limit documentation to
-- descriptions only. Removes documentation of
-- subitem tags such as `@param` and `@field` ([[#Item
-- subtags|see list]]).
-- @field[opt] {boolean} options.sort Sort documentation items in
-- alphabetical order.
-- @field[opt] {boolean} options.strip Remove table index in
-- documentation.
-- @field[opt] {boolean} options.ulist Indent subitems as `<ul>`
-- lists (LDoc/JSDoc behaviour).
-- @field[opt] {boolean} options.verbose Show the code
-- description of a package function when documenting
-- function modules.
-- Docbunto private utilities.
local utils = {}
--- @{string.find} optimisation for @{string} functions.
-- @function utils.strfind_wrap
-- @param {function} strfunc String library function.
-- @return {function} Function wrapped in @{string.find} check.
-- @local
function utils.strfind_wrap(func)
return function(...)
local arg = {...}
if string.find(arg[1], arg[2]) then
return func(...);
end
end
end
--- String concatenation utility.
-- @function join
-- @param {sequence<string>} tbl Table of string segments
-- @return {function} A concatenated string.
-- @local
function utils.join(tbl)
return table.concat(tbl, '')
end
--- Pattern configuration function.
-- Resets patterns for each documentation build.
-- @function utils.configure_patterns
-- @param {options} options Configuration options.
-- @local
function utils.configure_patterns(options)
-- Setup Unicode or ASCII character encoding (optimisation).
gsub = utils.strfind_wrap(
options.unicode
and mw.ustring.gsub
or string.gsub
)
match = utils.strfind_wrap(
options.unicode
and mw.ustring.match
or string.match
)
patterns.DOCBUNTO_SUMMARY =
options.iso639_th
and '^[^ ]+'
or
options.unicode
and '^[^.։。।෴۔።]+[.։。।෴۔።]?'
or '^[^.]+%.?'
patterns.DOCBUNTO_CONCAT = options.iso639_th and '' or ' '
patterns.DOCBUNTO_TYPE = '^{({*[^}]+}*)}%s*'
patterns.DOCBUNTO_WIKITEXT = '^[{|!}:#*=]+[%s-}]+'
-- Setup parsing tag patterns with colon mode support.
patterns.DOCBUNTO_TAG = options.colon and '^%s*(%w+):' or '^%s*@(%w+)'
patterns.DOCBUNTO_TAG_COMMENT = '^[-%s]*' .. patterns.DOCBUNTO_TAG:sub(2)
patterns.DOCBUNTO_TAG_VALUE = patterns.DOCBUNTO_TAG .. '(.*)'
patterns.DOCBUNTO_TAG_MOD_VALUE = patterns.DOCBUNTO_TAG .. '%[([^%]]*)%](.*)'
patterns.DOCBUNTO_TAG_OPTION_VALUE = '%[([^%]]*)%]%s*(.*)'
end
--- Tag processor function.
-- @function utils.process_tag
-- @param {string} str Tag string to process.
-- @return {table} Tag object.
-- @local
function utils.process_tag(str)
local tag = {}
if str:find(patterns.DOCBUNTO_TAG_MOD_VALUE) then
tag.name, tag.modifiers, tag.value = str:match(patterns.DOCBUNTO_TAG_MOD_VALUE)
local modifiers = {}
for m in tag.modifiers:gmatch('[^%s,]+') do
local i = m:find('=')
if i then
local key = mw.text.trim(m:sub(1, i - 1))
local val = mw.text.trim(m:sub(i + 1, #m))
modifiers[key] = val
else
modifiers[m] = true
end
end
if modifiers.optchain then
modifiers.opt = true
modifiers.optchain = nil
end
tag.modifiers = modifiers
else
tag.name, tag.value = str:match(patterns.DOCBUNTO_TAG_VALUE)
end
tag.value = mw.text.trim(tag.value)
if p.tags._type_alias[tag.name] then
if p.tags._type_alias[tag.name] ~= 'variable' then
tag.value = p.tags._type_alias[tag.name] .. ' ' .. tag.value
tag.name = 'field'
end
if tag.value:match('^%S+') ~= '...' then
tag.value = tag.value:gsub('^(%S+)', '{%1}')
end
end
tag.name = p.tags._alias[tag.name] or tag.name
if tag.name ~= 'usage' and tag.value:find(patterns.DOCBUNTO_TYPE) then
tag.type = tag.value:match(patterns.DOCBUNTO_TYPE)
if tag.type:find('^%?') then
tag.type = tag.type:sub(2) .. '|nil'
end
tag.value = tag.value:gsub(patterns.DOCBUNTO_TYPE, '')
end
if (tag.name == 'param' or tag.name == 'field') and tag.value:find(patterns.DOCBUNTO_TAG_OPTION_VALUE) then
tag.value = tag.value:gsub(patterns.DOCBUNTO_TAG_OPTION_VALUE, '%1 %2')
tag.modifiers = tag.modifiers or {}
tag.modifiers.opt = true
end
if p.tags[tag.name] == TAG_FLAG then
tag.value = true
end
return tag
end
--- Module info extraction utility.
-- @function utils.extract_info
-- @param {table} documentation Package doclet info.
-- @return {table} Information name-value map.
-- @local
function utils.extract_info(documentation)
local info = {}
for _, tag in ipairs(documentation.tags) do
if p.tags._module_info[tag.name] then
if info[tag.name] then
if not info[tag.name]:find('^%* ') then
info[tag.name] = '* ' .. info[tag.name]
end
info[tag.name] = info[tag.name] .. '\n* ' .. tag.value
else
info[tag.name] = tag.value
end
end
end
return info
end
--- Type extraction utility.
-- @function utils.extract_types
-- @param {table} item Item documentation data.
-- @return {sequence<string>} Item types.
-- @local
function utils.extract_types(item)
local item_types = {}
for _, tag in ipairs(item.tags) do
if p.tags[tag.name] == TAG_TYPE then
item_types[1] = tag.name
if tag.name == 'variable' then
local implied_local = utils.process_tag('@local')
table.insert(item.tags, mw.clone(implied_local))
item.tags['local'] = mw.clone(implied_local)
end
if
p.tags._generic_tags[item_types[1]] and
not p.tags._project_level[item_types[1]] and
tag.type
then
item_types[#item_types + 1] = tag.type
end
break
end
end
return item_types
end
--- Name extraction utility.
-- @function utils.extract_name
-- @param {table} item Item documentation data.
-- @param {boolean} project Whether the item is project-level.
-- @return {string} Item name.
-- @local
function utils.extract_name(item, opts)
opts = opts or {}
local item_name
for _, tag in ipairs(item.tags) do
if p.tags[tag.name] == TAG_TYPE then
item_name = tag.value; break
end
end
if item_name or not opts.project then
return item_name
end
item_name = item.code:match('\nreturn%s+([%w_]+)')
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 utils.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
function utils.deduce_name(tokens, index, opts)
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
--- Parses Lua item names to extract a hierarchy.
--- @function utils.parse_hierarchy
-- @param {string} item_name Documentation item name.
-- @return {sequence<string>} List of global keys to index the
-- variable reference.
-- @local
function utils.parse_hierarchy(item_name)
local hierarchy = {}
for _, lexeme in ipairs(lexer(item_name)) do
if lexeme.type == 'ident' or lexeme.type == 'string' or lexeme.type == 'number' then
hierarchy[#hierarchy + 1] = lexeme.text
end
end
return hierarchy
end
--- Code analysis utility.
-- @function utils.code_heuristic
-- @param {table} item Item documentation data.
-- @local
function utils.code_heuristic(item)
local is_section = item.tags['private'] or item.tags['local'] or item.type == 'type'
if item.name and item.type and is_section then
return
end
local tokens = lexer(item.code:match('^[^\n]*'))[1]
local t, i = tokens[1], 1
local item_name, item_type
local lexemes = {}
while t do
if t.type ~= 'whitespace' then
lexemes[#lexemes + 1] = tokens[i]
end
t, i = tokens[i + 1], i + 1
end
t, i = lexemes[1], 1
while t do
if t.data == '=' and not item.name then
item_name = utils.deduce_name(lexemes, i - 1, { lookbehind = true })
end
if t.data == 'function' and not item.name then
item_type = 'function'
if lexemes[i + 1] and lexemes[i + 1].data ~= '(' then
item_name = utils.deduce_name(lexemes, i + 1, { lookahead = true })
end
end
if t.data == '{' or t.data == '{}' and not item.type then
item_type = 'table'
end
if t.data == 'local' and not is_section then
local implied_local = utils.process_tag('@local')
table.insert(item.tags, implied_local)
item.tags['local'] = mw.clone(implied_local)
end
t, i = lexemes[i + 1], i + 1
end
item.name = item.name or item_name or ''
item.type = item.type or item_type
if #(item.types or {}) == 0 then
item.types[1] = item.type
end
end
--- Array hash map conversion utility.
-- @function utils.hash_map
-- @param {table} item Item documentation data array.
-- @return {table} Item documentation data map.
-- @local
function utils.hash_map(array)
local map = array
for _, element in ipairs(array) do
if map[element.name] and not map[element.name].name then
table.insert(map[element.name], mw.clone(element))
elseif map[element.name] and map[element.name].name then
map[element.name] = { map[element.name], mw.clone(element) }
else
map[element.name] = mw.clone(element)
end
end
return map
end
--- Item export utility.
-- @function utils.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
function utils.export_item(documentation, item_reference, item_index, item_alias, factory_item)
for _, item in ipairs(documentation.items) do
if item_reference == item.name then
item.tags['local'] = nil
item.tags['private'] = nil
for index, tag in ipairs(item.tags) do
if p.tags._privacy_tags[tag.name] then
table.remove(item.tags, index)
end
end
item.type, item.types[1] = 'member', 'member'
local accessor
if item_alias:find('^%[') then
accessor = ''
else
local is_class = documentation.type == 'classmod' or factory_item
if is_class and not item.tags['static'] then
accessor = ':'
else
accessor = '.'
end
end
if factory_item then
item.alias = utils.join{
documentation.items[item_index].tags['factory'].value,
accessor,
item_alias
}
else
item.alias = utils.join{
((documentation.tags['alias'] or {}).value or documentation.name),
accessor,
item_alias
}
end
item.hierarchy = utils.parse_hierarchy(item.alias)
break
end
end
end
--- Subitem tag correction utility.
-- @function utils.correct_subitem_tag
-- @param {table} item Item documentation data.
-- @local
function utils.correct_subitem_tag(item)
local field_tag = item.tags['field']
if item.type ~= 'function' or not field_tag then
return
end
if field_tag.name then
field_tag.name = 'param'
else
for _, tag_el in ipairs(field_tag) do
tag_el.name = 'param'
end
end
local param_tag = item.tags['param']
if param_tag and not param_tag.name then
if field_tag.name then
table.insert(param_tag, field_tag)
else
for _, tag_el in ipairs(field_tag) do
table.insert(param_tag, tag_el)
end
end
elseif param_tag and param_tag.name then
if field_tag.name then
param_tag = { param_tag, field_tag }
else
for i, tag_el in ipairs(field_tag) do
if i == 1 then
param_tag = { param_tag }
end
for _, tag_el in ipairs(field_tag) do
table.insert(param_tag, tag_el)
end
end
end
else
param_tag = field_tag
end
item.tags['field'] = nil
end
--- Item override tag utility.
-- @function utils.override_item_tag
-- @param {table} item Item documentation data.
-- @param {string} name Tag name.
-- @param[opt] {string} alias Target alias for tag.
-- @local
function utils.override_item_tag(item, name, alias)
if item.tags[name] then
item[alias or name] = item.tags[name].value
end
end
--- Identity function.
-- @function utils.identity
-- @param x The variable by reference.
-- @return The parameter `x` by reference.
-- @local
function utils.identity(x)
return x
end
-- Docbunto rendering logic.
local frontend = {}
--- Markdown header converter.
-- @function urils.markdown_header
-- @param {string} hash Leading hash.
-- @param {string} text Header text.
-- @return {string} MediaWiki header.
-- @local
function frontend.markdown_header(hash, text)
local symbol = '='
return utils.join{
'\n',
symbol:rep(#hash), ' ', text, ' ', symbol:rep(#hash),
'\n'
}
end
--- Item reference formatting.
-- @function urils.item_reference
-- @param {string} ref Item reference.
-- @return {string} Internal MediaWiki link to article item.
-- @local
function frontend.item_reference(ref)
local temp = mw.text.split(ref, '|')
local item = temp[1]
local text = temp[2] or temp[1]
if references.items[item] then
local interwiki = mw.site.server == DEV_WIKI and '' or 'w:c:dev:'
item = interwiki .. references.items[item]
else
item = '#' .. item
end
return mw.text.tag{
name = 'code',
content = utils.join{ '[[', item, '|', text, ']]' }
}
end
--- Doclet type reference preprocessor.
-- Formats types with links to the [[Lua reference manual]].
-- @function frontend.type_reference
-- @param {table} item Item documentation data.
-- @param {options} options Configuration options.
-- @local
function frontend.type_reference(item, options)
local interwiki = mw.site.server == DEV_WIKI and '' or 'w:c:dev:'
if
not options.noluaref and
item.value and
item.value:match('^%S+') == mw.text.tag('code', nil, '...')
then
item.value = item.value:gsub('^(%S+)', mw.text.tag{
name = 'code',
content = utils.join{ '[[', interwiki, 'Lua reference manual#varargs|...]]' }
})
end
-- Handle tag type references.
item.types = item.types or item.type and { item.type }
if not item.types then
return
end
for index, item_type in ipairs(item.types) do
item.types[index] = item.types[index]:gsub('[%w_. ]+', function(item_type)
local data = references.types[item_type]
local name = data and data.name or item_type
if not name:match('%.') and not name:match('^%u') and data then
name = i18n:msg('type-' .. name)
end
if data and not options.noluaref then
return utils.join{ '[[', interwiki, data.link, '|', name , ']]' }
elseif
item_type:find(i18n:msg('error-line'):gsub('$1', ''))
then
return utils.join { '[[' , options.file , '#L-', item_type:sub(6), '|', name, ']]' }
elseif
not options.noluaref and
not p.tags._generic_tags[item_type]
then
return utils.join { '[[#', item_type, '|', name, ']]' }
end
end)
end
item.type = table.concat(item.types, i18n:msg('separator-dot'))
end
--- Markdown preprocessor to MediaWiki format.
-- @function frontend.markdown
-- @param {string} str Unprocessed Markdown string.
-- @return {string} MediaWiki-compatible markup with HTML formatting.
-- @local
function frontend.markdown(str)
-- 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', frontend.markdown_header)
-- External and internal links.
str = str:gsub('%[([^\n%]]+)%]%(([^\n][^\n)]-)%)', '[%2 %1]')
str = str:gsub('%{@link%s+([^}]+)%}', '[%1]')
str = str:gsub('%@{([^\n}]+)}', frontend.item_reference)
-- Programming & scientific notation.
str = str:gsub('%f["`]`([^\n`]+)`%f[^"`]', '<code><nowiki>%1</nowiki></code>')
str = str:gsub('%$%$([^\n$]+)%$%$', '<math display="inline">%1</math>')
-- Strikethroughs.
str = str:gsub('~~([^\n~]+)~~', '<del>%1</del>')
-- HTML output.
return str
end
--- Doclet function item preprocessor.
-- Formats item name as a function call with top-level arguments.
-- @function frontend.pretask_function_name
-- @param {table} item Item documentation data.
-- @param {options} options Configuration options.
-- @local
function frontend.pretask_function_name(item, options)
local target = item.alias and 'alias' or 'name'
item[target] = item[target] .. '('
if
item.tags['param'] and
item.tags['param'].value and
not item.tags['param'].value:find('^[%w_]+[.[]')
then
if (item.tags['param'].modifiers or {})['opt'] then
local optional = mw.html.create('span')
optional:css('opacity', '0.65')
optional:wikitext(item.tags['param'].value:match('^(%S+)'))
item[target] = item[target] .. tostring(optional)
else
item[target] = item[target] .. item.tags['param'].value:match('^(%S*)')
end
elseif item.tags['param'] then
for index, tag in ipairs(item.tags['param']) do
if not tag.value:find('^[%w_]+[.[]') then
if (tag.modifiers or {})['opt'] then
local param = mw.html.create('span')
param:css('opacity', '0.65')
param:wikitext((index > 1 and ', ' or '') .. tag.value:match('^%S+'))
item[target] = item[target] .. tostring(param)
else
item[target] = item[target] .. (index > 1 and ', ' or '') .. tag.value:match('^(%S+)')
end
end
end
end
item[target] = item[target] .. ')'
end
--- Doclet parameter/field subitem preprocessor.
-- Indents and wraps variable prefix with `code` tag.
-- @function pretask_variable_prefix
-- @param {table} item Item documentation data.
-- @param {options} options Configuration options.
-- @local
function frontend.pretask_variable_prefix(item, options)
local indent_symbol = options.ulist and '*' or ':'
local indent_level, indentation
if item.value then
indent_level = item.value:match('^%S*') == '...'
and 0
or select(2, item.value:match('^%S*'):gsub('[.[]', ''))
indentation = indent_symbol:rep(indent_level)
item_id = options.item_id
item.value = indentation .. item.value:gsub(
'^(%S+)',
mw.text.tag{
name = 'code',
attrs = { id = item_id .. '~%1' },
content = '%1'
},
1
)
elseif item then
for _, item_el in ipairs(item) do
frontend.pretask_variable_prefix(item_el, options)
end
end
end
--- Doclet usage subitem preprocessor.
-- Formats usage example with `<syntaxhighlight>` tag.
-- @function frontend.pretask_usage_highlight
-- @param {table} item Item documentation data.
-- @param {options} options Configuration options.
-- @local
function frontend.pretask_usage_highlight(item, options)
if item.value then
item.value = unindent(mw.text.trim(item.value))
if item.value:find('^{{.+}}$') then
item.value = item.value:gsub('=', mw.text.nowiki)
local multi_line = item.value:find('\n') and '|m = 1|' or '|'
if item.value:match('^{{([^:]+)') == '#invoke' then
item.value = item.value:gsub('^{{[^:]+:', '{{t|i = 1' .. multi_line)
else
if options.entrypoint then
item.value = item.value:gsub('^([^|]+)|%s*([^|}]-)(%s*)([|}])','%1|"%2"%3%4')
end
item.value = item.value:gsub('^{{', '{{t' .. multi_line)
end
local highlight_class = tonumber(mw.site.currentVersion:match('^%d%.%d+')) > 1.19
and 'mw-highlight'
or 'mw-geshi'
if item.value:find('\n') then
highlight_div = mw.html.create('div')
highlight_div.addClass(highlight_class)
highlight_div.addClass('mw-content-ltr')
highlight_div:attr('dir', 'ltr')
highlight_div:wikitext(item.value)
item.value = tostring(highlight_div)
else
item.value = mw.text.tag{
name = 'span',
attrs = { class = 'code' },
content = item.value
}
end
else
highlight_tag = mw.html.create('syntaxhighlight')
highlight_tag:attr('lang', 'lua')
if not item.value:find('\n') then
highlight_tag:attr('inline', 'inline')
end
highlight_tag:wikitext(item.value)
item.value = tostring(highlight_tag)
end
elseif item then
for _, item_el in ipairs(item) do
frontend.pretask_usage_highlight(item_el, options)
end
end
end
--- Doclet error subitem preprocessor.
-- Formats line numbers (`{#}`) in error tag values.
-- @function frontend.pretask_error_line
-- @param {table} item Item documentation data.
-- @local
function frontend.pretask_error_line(item, options)
if item.name then
local line
if item.modifiers and item.modifiers.line then
line = item.modifiers.line
end
for mod in pairs(item.modifiers or {}) do
if mod:find('^%d+$') then line = mod end
end
if line then
if item.types then
item.types[#item.types + 1] = i18n:msg('error-line', line)
else
item.type = i18n:msg('error-line', line)
end
end
elseif item then
for _, item_el in ipairs(item) do
frontend.pretask_error_line(item_el, options)
end
end
end
--- Doclet item renderer.
-- @function frontend.render_item
-- @param {table} stream Wikitext documentation stream.
-- @param {table} item Item documentation data.
-- @param {options} options Configuration options.
-- @param[opt] {function} pretask Item data preprocessor.
-- @local
function frontend.render_item(stream, item, options, pretask)
if pretask then pretask(item, options) end
local item_name = item.alias or item.name
frontend.type_reference(item, options)
local item_type = item.type
for _, name in ipairs(p.tags._subtype_hierarchy) do
if item.tags[name] then
item_type = item_type .. i18n:msg('separator-dot') .. name
end
end
item_type = i18n:msg('parentheses', item_type)
if options.strip and item.export and item.hierarchy then
item_name = item_name:gsub('^[%w_]+[.[]?', '')
end
stream
:wikitext(';')
:tag('code')
:attr('id', options.item_id)
:wikitext(item_name)
:done():done()
:wikitext(item_type)
:newline()
if (#(item.summary or '') + #item.description) ~= 0 then
local separator = #(item.summary or '') ~= 0 and #item.description ~= 0
and (item.description:find(patterns.DOCBUNTO_WIKITEXT) and '\n' or patterns.DOCBUNTO_CONCAT)
or ''
local intro = (item.summary or '') .. separator .. item.description
intro = intro
:gsub('\n([{:#*])', '\n:%1')
:gsub('\n\n([^=])', '\n:%1')
stream:wikitext(':' .. intro):newline()
end
end
--- Doclet tag renderer.
-- @function frontend.render_tag
-- @param {table} stream Wikitext documentation stream.
-- @param {string} name Item tag name.
-- @param {table} tag Item tag data.
-- @param {options} options Configuration options.
-- @param[opt] {function} pretask Item data preprocessor.
-- @local
function frontend.render_tag(stream, name, tag, options, pretask)
if pretask then pretask(tag, options) end
if tag.value then
frontend.type_reference(tag, options)
local tag_name = i18n:msg('tag-' .. name, '1')
-- Handle ul/ol/dl comment support.
stream:wikitext(':')
:tag('b')
:wikitext(tag_name)
:done():done()
:wikitext(i18n:msg('separator-semicolon'))
:wikitext((mw.text.trim(tag.value):gsub('\n([{:#*])', '\n:%1')))
if tag.value:find('\n[{:#*]') and (tag.type or (tag.modifiers or {})['opt']) then
stream:newline():wikitext(':')
end
-- Type and optional appositive for tag.
if tag.type and (tag.modifiers or {})['opt'] then
stream:wikitext(i18n:msg{
key = 'parentheses',
args = {
tag.type ..
i18n:msg('separator-colon') ..
i18n:msg('optional')
}
})
elseif tag.type then
stream:wikitext(i18n:msg{
key = 'parentheses',
args = { tag.type }
})
elseif (tag.modifiers or {})['opt'] then
stream:wikitext(i18n:msg{
key = 'parentheses',
args = { i18n:msg('optional') }
})
end
stream:newline()
else
local tag_name = i18n:msg('tag-' .. name, tostring(#tag))
stream
:wikitext(':')
:tag('b')
:wikitext(tag_name)
:done():done()
:wikitext(i18n:msg('separator-semicolon'))
:newline()
for _, tag_el in ipairs(tag) do
frontend.type_reference(tag_el, options)
local marker = options.ulist and '*' or ':'
stream:wikitext(':' .. marker .. tag_el.value:gsub('\n([{:#*])', '\n:' .. marker .. '%1'))
if tag_el.value:find('\n[{:#*]') and (tag_el.type or (tag_el.modifiers or {})['opt']) then
stream:newline():wikitext(':' .. marker .. (tag_el.value:match('^[*:]+') or ''))
end
if tag_el.type and (tag_el.modifiers or {})['opt'] then
stream:wikitext(i18n:msg{
key = 'parentheses',
args = {
tag_el.type ..
i18n:msg('separator-colon') ..
i18n:msg('optional')
}
})
elseif tag_el.type then
stream:wikitext(i18n:msg{
key = 'parentheses',
args = { tag_el.type }
})
elseif (tag_el.modifiers or {})['opt'] then
stream:wikitext(i18n:msg{
key = 'parentheses',
args = { i18n:msg('optional') }
})
end
stream:newline()
end
end
end
--- Template entrypoint for [[Template:Docbunto]].
-- @function p.main
-- @param {Frame} f Scribunto frame object.
-- @return {string} Module documentation output.
function p.main(f)
frame = f:getParent()
local modname = mw.text.trim(frame.args[1] or frame.args.file or DEFAULT_TITLE)
local options = {}
options.file = modname
options.all = yesno(frame.args.all, false)
options.boilerplate = yesno(frame.args.boilerplate, false)
options.box = yesno(frame.args.box, title.namespace == 0)
options.caption = frame.args.caption
options.card = yesno(frame.args.card, title.namespace == 828)
options.code = yesno(frame.args.code, false)
options.colon = yesno(frame.args.colon, false)
options.footer = yesno(frame.args.footer, title.namespace == 828)
options.image = frame.args.image
options.migration = yesno(frame.args.migration, false)
options.noluaref = yesno(frame.args.noluaref, false)
options.plain = yesno(frame.args.plain, false)
options.preface = frame.args.preface
options.simple = yesno(frame.args.simple, false)
options.sort = yesno(frame.args.sort, false)
options.strip = yesno(frame.args.strip, false)
options.verbose = yesno(frame.args.verbose, false)
options.ulist = yesno(frame.args.ulist, false)
return p.build(modname, options)
end
--- Scribunto documentation generator entrypoint.
-- @function p.build
-- @param[opt] {string} modname Module page name (without namespace).
-- Default: second-level subpage.
-- @param[opt] {options} options Configuration options.
function p.build(modname, options)
modname = modname or DEFAULT_TITLE
options = options or {}
local tagdata = p.taglet(modname, options)
local docdata = p.doclet(tagdata, options)
return docdata
end
--- Docbunto taglet parser for Scribunto modules.
-- @function p.taglet
-- @param[opt] {string} modname Module page name (without namespace).
-- @param[opt] {options} options Configuration options.
-- @error[line=1081] {string} 'Lua source code not found in $1'
-- @error[line=1087] {string} 'documentation markup for Docbunto not found in $1'
-- @return {table} Module documentation data.
function p.taglet(modname, options)
modname = modname or DEFAULT_TITLE
options = options or {}
local filepath = mw.site.namespaces[828].name .. ':' .. modname
local content = mw.title.new(filepath):getContent()
-- Content checks.
if not content then
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(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'
utils.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.code = content
documentation.comments = {}
documentation.tags = {}
documentation.items = {}
local line_no = 1
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 factory_mode = false
local return_mode = false
local comment_tail = ''
local tag_name = ''
local new_item = false
local new_tag = false
local new_tag_item = false
local new_item_code = false
local pretty_comment = false
local comment_brace = false
local t, i, item = tokens[1], 1
local p_success, p_error = pcall(function()
while t do
-- Taglet variable update.
new_item = t.data:find('^%-%-%-') or t.data:find('^%-%-%[%=*%[$')
comment_tail = t.data:gsub('^%-%-+', '')
tag_name = comment_tail:match(patterns.DOCBUNTO_TAG)
tag_name = p.tags._alias[tag_name] or tag_name
new_tag = type(p.tags[tag_name]) == 'string'
local last_token = t.posFirst ~= 1 and 2 or 1
new_tag_item = (
new_tag and i > 1 and tokens[i - last_token].type ~= 'comment'
and not tokens[i - last_token].data:find(patterns.DOCBUNTO_TAG_COMMENT)
)
pretty_comment =
t.data:find('^%-%-%-+%s*$') or
t.data:find('[^-]+%-%-%-+%s*$') or
t.data:find('%-*[ \t]*<nowiki>$') or
t.data:find('%-*[ \t]*<pre>$')
comment_brace =
t.data:find('^%-%-%[=*%[$') or
t.data:find('^%-%-%]=*%]$') or
t.data:find('^%]=*%]%-%-$')
pragma_mode = tag_name == 'pragma'
export_mode = tag_name == 'export' and not doctag_mode
special_tag = pragma_mode or export_mode
local tags, tag, subtokens, separator
-- 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
if (new_tag or new_tag_item) and not special_tag then
comment_mode = true
doctag_mode = true
end
-- Module-level documentation taglet.
if start_mode then
table.insert(documentation.comments, t.data)
end
-- Module description.
if
start_mode and comment_mode and
not new_tag and not doctag_mode and
not pretty_comment and not comment_brace
then
separator = mw.text.trim(comment_tail):find(patterns.DOCBUNTO_WIKITEXT)
and '\n'
or (#documentation.description ~= 0 and patterns.DOCBUNTO_CONCAT or '')
documentation.description = utils.join{
documentation.description,
separator,
mw.text.trim(comment_tail)
}
end
-- Switch from module description to module-level tag list.
if start_mode and new_tag and not export_mode then
doctag_mode = true
table.insert(documentation.tags, utils.process_tag(comment_tail))
-- Concatenating module-level tag with more information.
elseif
start_mode and doctag_mode and
not pretty_comment and not comment_brace
then
tags = documentation.tags
if p.tags[tags[#tags].name] == TAG_MULTI then
separator = mw.text.trim(comment_tail):find(patterns.DOCBUNTO_WIKITEXT)
and '\n'
or patterns.DOCBUNTO_CONCAT
tags[#tags].value = utils.join{
tags[#tags].value,
separator,
mw.text.trim(comment_tail)
}
elseif p.tags[tags[#tags].name] == TAG_MULTI_LINE then
tags[#tags].value = utils.join{
tags[#tags].value,
'\n',
comment_tail
}
end
end
-- Detect a new documentation item.
if not start_mode and (new_item or new_tag_item) and not special_tag then
table.insert(documentation.items, {})
item_index = item_index + 1
item = documentation.items[item_index]
item.lineno = line_no
item.code = ''
item.comments = {}
item.description = ''
item.tags = {}
end
-- Concatenate an item with more information.
if
not start_mode and comment_mode and
not new_tag and not doctag_mode and
not pretty_comment and not comment_brace
then
separator = (mw.text.trim(comment_tail):find(patterns.DOCBUNTO_WIKITEXT) or fence_mode)
and '\n'
or (#item.description ~= 0 and patterns.DOCBUNTO_CONCAT or '')
item.description = utils.join{
item.description,
separator,
mw.text.trim(comment_tail)
}
end
-- Switch from item information to item tags.
if not start_mode and new_tag and not special_tag then
doctag_mode = true
table.insert(item.tags, utils.process_tag(comment_tail))
-- Extend item tags with lines of further information.
elseif
not start_mode and doctag_mode and
not pretty_comment and not comment_brace
then
tags = item.tags
if p.tags[tags[#tags].name] == TAG_MULTI then
separator = mw.text.trim(comment_tail):find(patterns.DOCBUNTO_WIKITEXT)
and '\n'
or patterns.DOCBUNTO_CONCAT
tags[#tags].value = utils.join{
tags[#tags].value,
separator,
mw.text.trim(comment_tail)
}
elseif p.tags[tags[#tags].name] == TAG_MULTI_LINE then
tags[#tags].value = utils.join{
tags[#tags].value,
'\n',
comment_tail
}
end
end
-- Save pre-item comments to item information.
if not start_mode and (comment_mode or doctag_mode) then
table.insert(item.comments, t.data)
end
-- Export tag support.
if export_mode then
factory_mode = t.posFirst ~= 1
if factory_mode then
item.exports = true
else
documentation.exports = true
end
-- Parse export token data while concatenating code.
subtokens = {}
while t and (not factory_mode or (factory_mode and t.data ~= 'end')) do
if factory_mode then
item.code = utils.join{
item.code,
(t.posFirst == 1 and '\n' or ''),
t.data
}
end
-- Fetch our item export tokens aka "subtokens".
t, i = tokens[i + 1], i + 1
if t and t.posFirst == 1 then
line_no = line_no + 1
end
if t and t.type ~= 'whitespace' and t.type ~= 'keyword' and t.type ~= 'comment' then
table.insert(subtokens, t)
end
end
-- Tracking and boolean variables for parsing exports.
local separator = { [','] = true, [';'] = true }
local brace = { ['{'] = true, ['}'] = true }
local item_reference, item_alias = '', ''
local sequence_index, has_key = 0, false
local subtoken, index = subtokens[2], 2
-- Logic that exports items to an item or module.
while not brace[subtoken.data] do
if subtoken.data == '=' then
has_key = true
elseif not separator[subtoken.data] then
if has_key then
item_reference = item_reference .. subtoken.data
else
item_alias = item_alias .. subtoken.data
end
end
if subtokens[index + 1] and separator[subtokens[index + 1].data] or brace[subtokens[index + 1].data] then
if not has_key then
sequence_index = sequence_index + 1
item_reference, item_alias = item_alias, item_reference
item_alias = '[' .. tostring(sequence_index) .. ']'
end
utils.export_item(documentation, item_reference, item_index, item_alias, factory_mode)
item_reference, item_alias, has_key = '', '', false
end
subtoken, index = subtokens[index + 1], index + 1
end
if not factory_mode then
break -- end of file
else
factory_mode = false -- end of item
end
end
-- Pragma tag support.
if pragma_mode then
tag = utils.process_tag(comment_tail)
if tag.modifiers then
options[tag.value] = yesno((next((tag ).modifiers or {})), true)
elseif tag.value:find(' ') then
local i = tag.value:find(' ')
local key = tag.value:sub(1, i - 1)
local val = tag.value:sub(i + 1)
options[key] = yesno(val)
elseif options[tag.value] == nil then
options[tag.value] = true
end
end
end
-- Package data post-processing.
if (t.type ~= 'comment' or t.data:find('^%-%-%]=*%]$') or t.data:find('^%]=*%]%-%-$')) and (comment_mode or doctag_mode) and start_mode then
documentation.tags = utils.hash_map(documentation.tags)
documentation.name = utils.extract_name(documentation, { project = true })
documentation.info = utils.extract_info(documentation)
documentation.types = utils.extract_types(documentation) or { 'module' }
documentation.type = documentation.types[1]
-- N.B: summary field = 1st comment sentence; description = text remainder.
if #documentation.description ~= 0 then
documentation.summary = match(documentation.description, patterns.DOCBUNTO_SUMMARY)
documentation.description = gsub(documentation.description, patterns.DOCBUNTO_SUMMARY .. '%s*', '', 1)
end
documentation.description = documentation.description:gsub('%s%s+', '\n\n')
documentation.executable = p.tags._code_types[documentation.type] and true or false
utils.correct_subitem_tag(documentation)
utils.override_item_tag(documentation, 'name')
utils.override_item_tag(documentation, 'alias')
utils.override_item_tag(documentation, 'summary')
utils.override_item_tag(documentation, 'description')
utils.override_item_tag(documentation, 'class', 'type')
end
-- Item data post-processing.
if
(t.type ~= 'comment' or t.data:find('^%-%-%]=*%]$') or t.data:find('^%]=*%]%-%-$'))
and (comment_mode or doctag_mode) and item_index ~= 0
then
item.tags = utils.hash_map(item.tags)
item.name = utils.extract_name(item)
item.types = utils.extract_types(item)
item.type = item.types[1]
if #item.description ~= 0 then
item.summary = match(item.description, patterns.DOCBUNTO_SUMMARY)
item.description = gsub(item.description, patterns.DOCBUNTO_SUMMARY .. '%s*', '')
end
item.description = item.description:gsub('%s%s+', '\n\n')
new_item_code = true
end
-- Documentation block reset.
if t.type ~= 'comment' or t.data:find('^%-%-%]=*%]$') or t.data:find('^%]=*%]%-%-$') then
start_mode = false
comment_mode = false
doctag_mode = false
export_mode = false
pragma_mode = false
end
-- Don't concatenate module return value into item code.
if t.data == 'return' and t.posFirst == 1 then
return_mode = true
end
-- Item code concatenation.
if item_index ~= 0 and not doctag_mode and not comment_mode and not comment_brace and not return_mode then
separator = #item.code ~= 0 and t.posFirst == 1 and '\n' or ''
item.code = utils.join{ item.code, separator, t.data }
-- Code analysis on item headline.
if new_item_code and item.code:find('\n') and t.posFirst == 1 then
utils.code_heuristic(item)
new_item_code = false
end
end
t, i = tokens[i + 1], i + 1
end
documentation.lineno = line_no
local package_name = (documentation.tags['alias'] or {}).value or documentation.name or DEFAULT_VARIABLE
local package_alias = (documentation.tags['alias'] or {}).value or 'p'
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.tags['export'] or
(item.name and package_name and item.name:find(export_ptn:format(package_name))) or
(item.alias and package_name and item.alias:find(export_ptn:format(package_name)))
then
item.export = true
item.tags['local'] = nil
item.tags['private'] = nil
for index, tag in ipairs(item.tags) do
if tag.name == 'local' or tag.name == 'private' then
table.remove(item.tags, index)
end
end
end
if item.name and (item.name:find('[.:]') or item.name:find('%[[\'"]')) then
item.hierarchy = utils.parse_hierarchy(item.name)
end
item.type = item.type or ((item.alias or item.name or ''):find('[.[]') and 'member' or 'variable')
utils.correct_subitem_tag(item)
utils.override_item_tag(item, 'name')
utils.override_item_tag(item, 'alias')
utils.override_item_tag(item, 'summary')
utils.override_item_tag(item, 'description')
utils.override_item_tag(item, 'class', 'type')
end
-- Sort documentation items by their access level.
-- Items are sorted by whether they are exported at the module level.
-- Items are then subsorted by whether the item is `@local` aka `@private`.
table.sort(documentation.items, function(item1, item2)
local inaccessible1 = item1.tags['local'] or item1.tags['private']
local inaccessible2 = item2.tags['local'] or item2.tags['private']
-- Send package items to the top.
if item1.export and not item2.export then
return true
elseif item2.export and not item1.export then
return false
-- Send private items to the bottom.
elseif inaccessible1 and not inaccessible2 then
return false
elseif inaccessible2 and not inaccessible1 then
return true
-- Optional alphabetical sort.
elseif options.sort then
return (item1.alias or item1.name) < (item2.alias or item2.name)
-- Sort via source code order by default.
else
return item1.lineno < item2.lineno
end
end)
end)
if not p_success then
mw.log(p_error)
end
return documentation
end
--- Doclet renderer for Docbunto taglet data.
-- @function p.doclet
-- @param {table} data Taglet documentation data.
-- @param[opt] {options} options Configuration options.
-- @return {string} Wikitext documentation output.
function p.doclet(data, options)
local documentation = mw.html.create()
local namespace_ptn = utils.join{ '^', mw.site.namespaces[828].name, ':' }
local codepage = data.filename:gsub(namespace_ptn, '', 1)
options = options or {}
options.file = data.filename
frame = frame or mw.getCurrentFrame():getParent()
local maybe_md = options.plain and utils.identity or frontend.markdown
local tohtml = function(str)
return frame:preprocess(maybe_md(str))
end
-- Disable edit sections for automatic documentation pages.
if not options.code then
documentation:wikitext(frame:preprocess('__NOEDITSECTION__'))
end
-- Lua infobox for Fandom Developers Wiki.
if
not options.code and
mw.site.server == DEV_WIKI and
p.tags._code_types[data.type]
then
local infobox = {}
infobox.title = 'Infobox Lua'
infobox.args = {}
if codepage ~= mw.text.split(title.text, '/')[2] then
infobox.args['Title'] = codepage
infobox.args['Code'] = codepage
end
if options.image or data.info['image'] then
infobox.args['Image file'] = data.info['image']
end
if options.caption or data.info['caption'] then
infobox.args['Image caption'] = tohtml(
options.caption or data.info['caption']
)
end
infobox.args['Type'] = data.type == 'module' and 'invocable' or 'meta'
if data.info and data.info['release'] then
infobox.args['Status'] = data.info['release']
end
if data.summary then
infobox.args['Description'] = tohtml(data.summary)
end
if data.info and data.info['author'] then
infobox.args['Author'] = tohtml(data.info['author'])
end
if data.info and data.info['attribution'] then
infobox.args['Using code by'] = tohtml(data.info['attribution'])
end
if data.info and data.info['credit'] then
infobox.args['Other attribution'] = tohtml(data.info['credit'])
end
if data.info and data.info['require'] then
data.info['require'] = data.info['require']
:gsub('^[^[%s]+$', '[[%1]]')
:gsub('%* ([^[%s]+)', '* [[%1]]')
infobox.args['Dependencies'] = tohtml(data.info['require'])
end
if codepage ~= 'I18n' and data.code:find('[\'"]Dev:I18n[\'"]') or data.code:find('[\'"]Module:I18n[\'"]') then
infobox.args['Languages'] = 'auto'
elseif data.code:find('mw%.message%.new') then
infobox.args['Languages'] = 'mw'
end
if data.info and data.info['demo'] then
infobox.args['Examples'] = tohtml(data.info['demo'])
end
-- TODO: remove infobox auto generator.
if options.migration then
infobox = frame:preprocess(mw.text.tag{
name = 'syntaxhighlight',
content = require('Dev:FrameTools').expandTemplateMock(frame, infobox)
})
elseif not options.code and options.box then
infobox = frame:expandTemplate(infobox)
end
if options.migration or not options.code and options.box then
documentation:wikitext(infobox):newline()
end
-- Custom infobox for external wikis.
elseif not options.code and options.box then
local infoboxContent = mw.title.new('Module:Docbunto/infobox'):getContent()
local custom, infobox = pcall(require, 'Module:Docbunto/infobox')
if custom then
if infoboxContent and infoboxContent:find('frame:expandTemplate') then
mw.log('Module:Docbunto/infobox frame access is deprecated. Please remove frame:expandTemplate.')
end
if options.migration and type(infobox) == 'function' then
documentation:wikitext(frame:preprocess(mw.text.tag{
name = 'syntaxhighlight',
content = require('Dev:FrameTools').expandTemplateMock(frame, infobox)
}))
elseif type(infobox) == 'function' then
documentation:wikitext(frame:expandTemplate(infobox(data, codepage, frame, options))):newline()
end
end
end
-- Documentation lede.
if not options.code and (#(data.summary or '') + #data.description) ~= 0 then
local separator = #(data.summary or '') ~= 0 and #data.description ~= 0
and (data.description:find(patterns.DOCBUNTO_WIKITEXT) and '\n\n' or patterns.DOCBUNTO_CONCAT)
or ''
local intro = utils.join{ data.summary or '', separator, data.description }
intro = tohtml(intro:gsub('^(' .. codepage .. ')', mw.text.tag('b', nil, '%1')))
if options.migration then
documentation:wikitext(frame:preprocess(mw.text.tag{
name = 'syntaxhighlight',
content = intro
})):newline():newline()
else
documentation:wikitext(intro):newline():newline()
end
end
-- Custom documentation preface.
if options.preface then
documentation:wikitext(options.preface):newline():newline()
end
-- Start code documentation.
local codedoc = mw.html.create()
local function_module = data.tags['param'] or data.tags['return']
local header_type =
documentation.type == 'classmod'
and 'class'
or function_module
and 'function'
or 'items'
if (function_module or #data.items ~= 0) and not options.code or options.preface then
codedoc:wikitext('== ' .. i18n:msg('header-documentation') .. ' =='):newline()
end
if (function_module or #data.items ~= 0) then
codedoc:wikitext('=== ' .. i18n:msg('header-' .. header_type) .. ' ==='):newline()
end
-- Function module support.
if function_module then
data.type, data.types[1] = 'function', 'function'
options.item_id = data.name
if not options.code or not options.verbose then data.description = '' end
frontend.render_item(codedoc, data, options, frontend.pretask_function_name)
if not options.simple and data.tags['param'] then
frontend.render_tag(codedoc, 'param', data.tags['param'], options, frontend.pretask_variable_prefix)
end
if not options.simple and data.tags['error'] then
frontend.render_tag(codedoc, 'error', data.tags['error'], options, frontend.pretask_error_line)
end
if not options.simple and data.tags['return'] then
frontend.render_tag(codedoc, 'return', data.tags['return'], options)
end
end
-- Render documentation items.
local other_header = false
local private_header = false
local inaccessible
for _, item in ipairs(data.items) do
inaccessible = item.tags['local'] or item.tags['private']
if not options.all and inaccessible then
break
end
if
not other_header and item.type ~= 'section' and item.type ~= 'type' and
not item.export and not item.hierarchy and not inaccessible
then
codedoc:wikitext('=== ' .. i18n:msg('header-other') .. ' ==='):newline()
other_header = true
end
if not private_header and options.all and inaccessible then
codedoc:wikitext('=== ' .. i18n:msg('header-private') .. '==='):newline()
private_header = true
end
if item.type == 'section' then
codedoc:wikitext('=== ' .. mw.ustring.gsub(item.summary or item.alias or item.name, '[.։。।෴۔።]$', '') .. ' ==='):newline()
if #item.description ~= 0 then
codedoc:wikitext(mw.text.trim(item.description)):newline()
end
elseif item.type == 'type' then
if options.strip and item.export and item.hierarchy then
if item.alias then
item.alias = item.alias:gsub('^[%w_]+[.[]?', '')
else
item.name = item.name:gsub('^[%w_]+[.[]?', '')
end
end
codedoc:wikitext('=== ' .. mw.text.tag('code', {}, item.alias or item.name) .. ' ==='):newline()
if (#(item.summary or '') + #item.description) ~= 0 then
local separator = #(item.summary or '') ~= 0 and #item.description ~= 0
and (item.description:find(patterns.DOCBUNTO_WIKITEXT) and '\n\n' or patterns.DOCBUNTO_CONCAT)
or ''
codedoc:wikitext((item.summary or '') .. separator .. item.description):newline()
end
elseif item.type == 'function' then
options.item_id = item.alias or item.name
frontend.render_item(codedoc, item, options, frontend.pretask_function_name)
if not options.simple and item.tags['param'] then
frontend.render_tag(codedoc, 'param', item.tags['param'], options, frontend.pretask_variable_prefix)
end
if not options.simple and item.tags['error'] then
frontend.render_tag(codedoc, 'error', item.tags['error'], options, frontend.pretask_error_line)
end
if not options.simple and item.tags['return'] then
frontend.render_tag(codedoc, 'return', item.tags['return'], options)
end
elseif
item.type == 'table' or
item.type ~= nil and (
item.type == 'member' or
item.type == 'variable'
) and (item.alias or item.name)
then
options.item_id = item.alias or item.name
frontend.render_item(codedoc, item, options)
if not options.simple and item.tags['field'] then
frontend.render_tag(codedoc, 'field', item.tags['field'], options, frontend.pretask_variable_prefix)
end
end
if item.type ~= 'section' and item.type ~= 'type' then
if not options.simple and item.tags['note'] then
frontend.render_tag(codedoc, 'note', item.tags['note'], options)
end
if not options.simple and item.tags['warning'] then
frontend.render_tag(codedoc, 'warning', item.tags['warning'], options)
end
if not options.simple and item.tags['fixme'] then
frontend.render_tag(codedoc, 'fixme', item.tags['fixme'], options)
end
if not options.simple and item.tags['todo'] then
frontend.render_tag(codedoc, 'todo', item.tags['todo'], options)
end
if not options.simple and item.tags['usage'] then
frontend.render_tag(codedoc, 'usage', item.tags['usage'], options, frontend.pretask_usage_highlight)
end
if not options.simple and item.tags['see'] then
frontend.render_tag(codedoc, 'see', item.tags['see'], options)
end
end
end
-- Render module-level annotations.
local header_paren = options.code and '===' or '=='
local header_text
for _, tag_name in ipairs(p.tags._annotation_hierarchy) do
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
codedoc:newline():wikitext(header_text):newline()
if data.tags[tag_name].value then
codedoc:wikitext(data.tags[tag_name].value):newline()
else
for _, tag_el in ipairs(data.tags[tag_name]) do
codedoc:wikitext('* ' .. tag_el.value):newline()
end
end
end
end
-- Byline for autogeneration.
if options.footer then
codedoc:newline()
codedoc:tag('hr'):done()
codedoc:newline()
codedoc:wikitext(i18n:msg('message-autogeneration'))
end
-- Add nowiki tags for EOF termination in tests.
codedoc:tag('nowiki', { selfClosing = true })
-- Code documentation formatting.
codedoc = tohtml(tostring(codedoc))
documentation:wikitext(codedoc)
-- Endmatter table for module information.
if options.card then
local endmatter = mw.html.create('table')
endmatter:addClass('wikitable')
endmatter:css('width', '75%')
endmatter:css('margin', '0 auto 1em auto')
local module_type = data.type == 'module'
and i18n:msg('invocable-module')
or i18n:msg('meta-module')
endmatter
:tag('caption')
:wikitext('Module information card (autogenerated)')
endmatter
:tag('tr')
:tag('th'):attr('scope', 'col'):wikitext('Name'):done()
:tag('th'):attr('scope', 'col'):wikitext('Value'):done()
if options.image or data.info and data.info['image'] then
endmatter
:tag('tr')
:tag('td'):attr('scope', 'row'):wikitext(i18n:msg('endmatter-image')):done()
:tag('td')
:attr('align', 'center')
:wikitext('[[File:' .. (options.image or data.info['image']) .. '|270px|center]]')
:done()
:done()
end
if options.caption or data.info and data.info['caption'] then
endmatter
:tag('tr')
:tag('td'):attr('scope', 'row'):wikitext(i18n:msg('endmatter-caption')):done()
:tag('td')
:attr('align', options.image or data.info['image'] and 'center' or 'left')
:wikitext(tohtml(options.caption or data.info['caption']))
:done()
:done()
end
endmatter
:tag('tr')
:tag('td'):attr('scope', 'row'):wikitext((gsub(i18n:msg('type-variable'), '^%w', mw.ustring.upper))):done()
:tag('td')
:attr('align', options.image or data.info and data.info['image'] and 'center' or 'left')
:wikitext(mw.text.tag({
name = options.image or data.info and data.info['image'] and 'h1' or 'span',
content = mw.text.tag({
name = 'b',
content = mw.text.tag({
name = 'code',
content = data.name or DEFAULT_VARIABLE
})
})
})):done()
:done()
:tag('tr')
:tag('td'):wikitext(i18n:msg('code')):done()
:tag('td'):wikitext(tohtml('[[' .. data.filename .. ']]')):done()
:done()
if data.info and data.info['release'] then
endmatter
:tag('tr')
:tag('td'):attr('scope', 'row'):wikitext(i18n:msg('release-status')):done()
:tag('td'):wikitext(tohtml(data.info['release'])):done()
:done()
end
if data.summary then
endmatter
:tag('tr')
:tag('td'):attr('scope', 'row'):wikitext(i18n:msg('description')):done()
:tag('td'):wikitext(tohtml(data.summary)):done()
:done()
end
if data.info and data.info['author'] then
endmatter
:tag('tr')
:tag('td'):attr('scope', 'row'):wikitext(i18n:msg('author')):done()
:tag('td'):newline():wikitext(tohtml(data.info['author'])):done()
:done()
end
if data.info and data.info['attribution'] then
endmatter
:tag('tr')
:tag('td'):attr('scope', 'row'):wikitext(i18n:msg('using-code-by')):done()
:tag('td'):wikitext(tohtml(data.info['attribution'])):done()
:done()
end
if data.info and data.info['credit'] then
endmatter
:tag('tr')
:tag('td'):attr('scope', 'row'):wikitext(i18n:msg('other-attribution')):done()
:tag('td'):wikitext(tohtml(data.info['credit'])):done()
:done()
end
if data.info and data.info['require'] then
data.info['require'] = data.info['require']
:gsub('^[^[%s]+$', '[[%1]]')
:gsub('%* ([^[%s]+)', '* [[%1]]')
endmatter
:tag('tr')
:tag('td'):attr('scope', 'row'):wikitext(i18n:msg('dependencies')):done()
:tag('td'):newline():wikitext(tohtml(data.info['require'])):done()
:done()
end
if codepage ~= 'I18n' and data.code:find('[\'"]Dev:I18n[\'"]') or data.code:find('[\'"]Module:I18n[\'"]') then
local lang_query = '{{Language list| source-lua = Module:' .. codepage .. '/i18n | nocat = }}'
endmatter
:tag('tr')
:tag('td'):attr('scope', 'row'):wikitext(i18n:msg('lang-support')):done()
:tag('td'):wikitext(tohtml(lang_query)):done()
:done()
elseif data.code:find('mw%.message%.new') then
local mediawiki_lang = '[[' .. i18n:msg('system-messages') .. '|' .. i18n:msg('all-messages') .. ']]'
endmatter
:tag('tr')
:tag('td'):attr('scope', 'row'):wikitext(i18n:msg('lang-support')):done()
:tag('td'):wikitext(frame:preprocess(mediawiki_lang)):done()
:done()
end
if data.info and data.info['demo'] then
endmatter
:tag('tr')
:tag('td'):attr('scope', 'row'):wikitext(i18n:msg('examples')):done()
:tag('td'):wikitext(tohtml(data.info['demo'])):done()
:done()
end
endmatter:newline()
endmatter = tostring(endmatter)
documentation:newline():newline()
documentation:wikitext(endmatter)
end
documentation = tostring(documentation)
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'] = 'M', ['see'] = 'M', ['note'] = 'M', ['usage'] = 'ML',
['description'] = 'M', ['field'] = 'M', ['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 = {
['field'] = true,
['warning'] = true,
['fixme'] = true,
['note'] = true,
['todo'] = true,
['see'] = true
}
p.tags._annotation_hierarchy = {
'warning',
'fixme',
'note',
'todo',
'see'
}
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