dev

See Global Lua Modules/Docbunto

Subpages

--- 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
--  @license            MIT
--  @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.autodoc Whether the module is being
--                      called to automatically provide missing 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.subst Don't preprocess code output
--                      before the documentation is outputted e.g. `subst`.
--  @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           utils.join
--  @param              {sequence<string>} tbl Table of string segments
--  @return             {function} A concatenated string.
--  @local
function utils.join(tbl)
	return table.concat(tbl, '')
end

--- Check for `syntaxhighlight` extension.
--  @function           utils.testSyntaxHightlight
--	@param              {Frame} frame Frame in module context.
--  @return             {boolean} Whether SyntaxHighlight is available.
--  @local
function utils.testSyntaxHightlight(frame)
	return frame:extensionTag('syntaxhighlight') ~= "<syntaxhighlight></syntaxhighlight>"
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

--- Argument processing
-- @param				{string} funcName name of function
-- @return				function to process arguments
-- @local
function makeInvokeFunc(funcName)
	return function (f)
		local args = require("Dev:Arguments").getArgs(f, {
			valueFunc = function (key, value)
				if type(value) == 'string' then
					value = value:match('^%s*(.-)%s*$') -- Remove whitespace.
					if key == 'heading' or value ~= '' then
						return value
					else
						return nil
					end
				else
					return value
				end
			end
		})
		-- mw.log(p[funcName])
		return p[funcName](args)
	end
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.filepath , '#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

	local code_node = mw.html.create('code')
		:attr('id', options.item_id)
		:wikitext(item_name)

	if not options.content then
		local span_node = stream:wikitext(';')
			:tag('span')
			:addClass('plainlinks docbunto-link')
		if options.hugefile or not options.syntaxhighlight then
			span_node:wikitext('[{{fullurl:' .. options.filepath .. '|action=edit#mw-ce-l' .. item.lineno .. '}} ' .. tostring(code_node) .. ']')
		elseif title.namespace == 828 and options.filepath == title.text then
			span_node:wikitext('[[' .. '#L-' .. item.lineno .. '|' .. tostring(code_node) .. ']]')
		else
			span_node:wikitext('[[' .. options.filepath .. '#L-' .. item.lineno .. '|' ..  tostring(code_node) .. ']]')
		end
	else
		stream:wikitext(';' .. tostring(code_node))
	end

	stream
		: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(args)
	frame = mw.getCurrentFrame()
	local modname = mw.text.trim(args[1] or args.file or DEFAULT_TITLE)

	local options = {}
	options.file = modname
	options.all = yesno(args.all, false)
	options.autodoc = yesno(args.autodoc, false)
	options.boilerplate = yesno(args.boilerplate, false)
	options.box = yesno(args.box, title.namespace == 0)
	options.caption = args.caption
	options.card = yesno(args.card, title.namespace == 828)
	options.code = yesno(args.code, false)
	options.colon = yesno(args.colon, false)
	options.footer = yesno(args.footer, title.namespace == 828)
	options.image = args.image
	options.migration = yesno(args.migration, false)
	options.noluaref = yesno(args.noluaref, false)
	options.plain = yesno(args.plain, false)
	options.preface = args.preface
	options.simple = yesno(args.simple, false)
	options.subst = yesno(args.subst, false)
	options.sort = yesno(args.sort, false)
	options.strip = yesno(args.strip, false)
	options.verbose = yesno(args.verbose, false)
	options.ulist = yesno(args.ulist, false)

	return p.build(modname, options)
end

--- Entrypoint for the module.
--  @function		   p.main
--  @param			  {table} frame Module frame.
--  @return			 {string} Module documentation output.
p.main = makeInvokeFunc('_main')

--- 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=1144]   {string} 'Lua source code not found in $1'
--  @error[line=1154]   {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
	options.filepath = filepath

	-- Content checks.
	local content = mw.title.new(filepath):getContent()
	if options.content then
		content = options.content
	else
		content = mw.title.new(filepath):getContent() or error(i18n:msg('no-content', filepath), 0)
	end

	-- Syntax highlighting is disabled for files above a specific size
	options.hugefile = content:len() > 102400
	
	if
		not content:match('%-%-%-') and
		not content:match(options.colon and '%w+:' or '@%w+')
	then
		error(i18n:msg('no-markup', filepath))
	end
	
	options.syntaxhighlight = utils.testSyntaxHightlight(frame)

	-- 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 = 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 pragma_mode = 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 index = tag.value:find(' ')
					local key = tag.value:sub(1, index - 1)
					local val = tag.value:sub(index + 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 = documentation.description:sub(select(2, documentation.description:find('%s*', #documentation.summary + 1)) + 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 = item.description:sub(select(2, item.description:find('%s*', #item.summary + 1)) + 1)
			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.filepath = 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
			:tag('h2')
				:wikitext(i18n:msg('header-documentation'))
				:done()
			:newline()
	end
	if (function_module or #data.items ~= 0) then
		codedoc
			:tag('h3')
				:wikitext(i18n:msg('header-' .. header_type))
				:done()
			: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
				:tag('h3')
					:wikitext(i18n:msg('header-other'))
					:done()
				:newline()
			other_header = true
		end
		if not private_header and options.all and inaccessible then
			codedoc
				:tag('h3')
					:wikitext(i18n:msg('header-private'))
					:done()
				:newline()
			private_header = true
		end

		if item.type == 'section' then
			codedoc
				:tag('h3')
					:wikitext((mw.ustring.gsub(item.summary or item.alias or item.name, '[.։。।෴۔።]$', '')))
					:done()
				: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
				:tag('h3')
					:wikitext(mw.text.tag('code', {}, item.alias or item.name))
					:done()
				: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)
				options.todo = true
			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_level = options.code and 'h3' or 'h2'
	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_level .. '>' .. header_text .. '</' .. header_level .. '>'
			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

	-- Code documentation formatting.
	codedoc = tostring(codedoc)
	if not options.subst then
		codedoc = tohtml(codedoc)
	else
		codedoc = maybe_md(codedoc)
	end
	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
			:newline()
			:tag('caption')
			:wikitext('Module information card (autogenerated)')
			:done():newline()
	
		endmatter
			:tag('tr')
				:tag('th'):attr('scope', 'col'):wikitext('Name'):done()
				:tag('th'):attr('scope', 'col'):wikitext('Value'):done()
			:done():newline()
	
		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()
				:newline()
		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()
				:newline()
		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()
				:newline()
	
		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()
				:newline()
		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()
				:newline()
		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()
				:newline()
		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()
				:newline()
		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()
				:newline()
		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()
				:newline()
		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()
				:newline()
		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()
				:newline()
		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()
				:newline()
		end

		endmatter = tostring(endmatter)
		documentation:newline():newline()
		documentation:wikitext(endmatter)
	end

	documentation = tostring(documentation)
	if options.todo and options.autodoc 
		and mw.title.new('Module:' .. options.file).fullText == mw.title.getCurrentTitle().fullText then
		documentation = documentation .. '[[Category:' .. i18n:msg('todo') .. ']]'
	end
	if mw.title.new('Module:Docbunto/styles.css').exists then
		documentation = documentation .. frame:extensionTag('templatestyles', '', {src = 'Module:Docbunto/styles.css'})
	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'] = '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