Module:Docbunto: Difference between revisions
ReisuDesign (talk | contribs) Created page with "--- Docbunto, based on 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 --..." |
ReisuDesign (talk | contribs) No edit summary |
||
| (One intermediate revision by the same user not shown) | |||
| Line 54: | Line 54: | ||
local TAGGROUP_TAG_CLASS = 'docbunto-taggroup-tag' | local TAGGROUP_TAG_CLASS = 'docbunto-taggroup-tag' | ||
local TAG_KEYS_CLASS = 'docbunto-tag-keys' | local TAG_KEYS_CLASS = 'docbunto-tag-keys' | ||
local NEEDS_NEWLINE = '^[{|!};:#*=]' -- tables, dl, ul, ol, headings | local NEEDS_NEWLINE = '^[{|!};:#*=]' -- tables, dl, ul, ol, headings | ||
| Line 78: | Line 75: | ||
-- Module dependencies. | -- Module dependencies. | ||
local title = mw.title.getCurrentTitle() | local title = mw.title.getCurrentTitle() | ||
local i18n = require(' | local i18n = require('Module:I18n').loadMessages('Module:Docbunto/i18n') | ||
local references = mw.loadData(' | local references = mw.loadData('Module:Docbunto/references') | ||
local lexer = require(' | local lexer = require('Module:Lexer') | ||
local unindent = require(' | local unindent = require('Module:Unindent') | ||
local yesno = require(' | local yesno = require('Module:Yesno') | ||
-- Module variables. | -- Module variables. | ||
| Line 192: | Line 189: | ||
if references.items[item] then | if references.items[item] then | ||
item = | item = references.items[item] | ||
elseif item:find('#', 1, true) then | elseif item:find('#', 1, true) then | ||
item = mw.site.namespaces[828].name .. ':' .. item | item = mw.site.namespaces[828].name .. ':' .. item | ||
| Line 584: | Line 581: | ||
if not options.noluaref and item.name == '...' then | if not options.noluaref and item.name == '...' then | ||
item.name = '[[' | item.name = '[[' .. 'Lua reference manual#varargs|...]]' | ||
end | end | ||
| Line 607: | Line 604: | ||
if data then | if data then | ||
if not options.noluaref then | if not options.noluaref then | ||
types[i] = '[[' | types[i] = '[[' .. data.link .. '|' .. name .. ']]' | ||
end | end | ||
else | else | ||
Latest revision as of 15:14, 2 September 2026
| Main | Documentation | Usage |
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.
Documentation
Package items
docbunto.main(f)(function)Template entrypoint for Template:Docbunto.
- Parameter:
(table)
fScribunto frame object.
- Returns:
(string) Module documentation output.
docbunto.build([options])(function)Scribunto documentation generator entrypoint.
- Parameters:
(table;optional)
optionsConfiguration options.- Keys:
(string;optional)
options.fileModule page name (without namespace). Default: second-level subpage.(string;optional)
options.contentAn parameter used for debugging this module. Pass in a string containing source code to view the documentation it would generate.(boolean;optional)
options.allInclude local items in documentation.(boolean;optional)
options.boilerplateRemoval of boilerplate (license block comments).(string;optional)
options.captionInfobox image caption.(boolean;optional)
options.codeOnly document Docbunto code items - exclude article infobox and lede from rendered documentation. Permits article to be edited in VisualEditor.(boolean;optional)
options.colonFormat tags with a:suffix and without the@prefix. This bypasses the "doctag soup" some authors complain of.(string;optional)
options.imageInfobox image.(boolean;optional)
options.noluarefDon't link to the Lua reference manual for types.(boolean;optional)
options.plainDisable Markdown formatting in documentation.(string;optional)
options.prefacePreface text to insert between lede & item documentation, used to provide usage and code examples.(boolean;optional)
options.stripRemove table index in documentation.
docbunto.taglet([options])(function)
docbunto.doclet(data, [options])(function)
docbunto.tags(table)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.
- Multi-line tags use the
--- 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 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('Module:I18n').loadMessages('Module:Docbunto/i18n')
local references = mw.loadData('Module:Docbunto/references')
local lexer = require('Module:Lexer')
local unindent = require('Module:Unindent')
local yesno = require('Module: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 = 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 = '[[' .. '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] = '[[' .. 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